Open20

go-zeroを学ぶ2

135yshr135yshr

Goctl サブコマンド集

闇雲にやらないように goctl のコマンドを整理
全部で11個コマンドがあるもよう

api と rpc は、Getting Started で使っているので飛ばして、model, docker, kube, template 辺りをまとめてみる

  1. api
  2. rpc
  3. model
  4. docker
  5. kube
  6. migrate
  7. template
  8. env
  9. complation
  10. upgrade
  11. bug

概要図

135yshr135yshr

Model サブコマンドから始める

データベースを使う要件はよくあると思われるので、今回は、model をまとめてみる。
データベース使うときによく話にあがってくるのが、この辺なのでどのくらい楽になるか調査してみようとおもう

  1. migration どうしよう
  2. バージョン管理どうしよう
  3. テーブルとモデルの乖離どうしよう
  4. とかとか
135yshr135yshr

model サブコマンド

model サブコマンドは、モデル層のコードを生成するためのコマンドらしい。

MySQLのDDL読み込んでコード生成できたり、データベースに接続して、モデルコード作成もやってくれたり、至れり尽くせり。(要検証だけど)

対応するデータベース

  • MySQL(mysql)
  • MongoDB(mongdb)
  • PostgreSQL(pg)

goctl model の後に、mysqlmongo pg を指定すると対応するデータベースに合わせてコードを生成してくれるとのこと

あとは、ドキュメントを斜め読みしつつ翻訳していく。

goctl model mysql -h
Generate mysql model

Usage:
  goctl model mysql [command]

Available Commands:
  datasource  Generate model from datasource
  ddl         Generate mysql model from ddl

Flags:
  -h, --help                     help for mysql
  -i, --ignore-columns strings   Ignore columns while creating or updating rows (default [create_at,created_at,create_time,update_at,updated_at,update_time])
      --strict                   Generate model in strict mode

デフォルトルール

デフォルトでは、ユーザーはテーブルを作成する際にcreateTimeとupdateTimeフィールド(大文字小文字とアンダースコアの命名スタイルを無視)を作成し、デフォルト値は両方ともCURRENT_TIMESTAMPで、updateTimeはON UPDATE CURRENT_TIMESTAMPをサポートします。これら2つのフィールドは、挿入や更新の割り当て範囲にない場合に削除されます。もちろん、これら2つのフィールドが必要ない場合は問題ありません。

135yshr135yshr

ddl

goctl model mysql ddl -h
Generate mysql model from ddl

Usage:
  goctl model mysql ddl [flags]

Flags:
      --branch string     The branch of the remote repo, it does work with --remote
  -c, --cache             Generate code with cache [optional]
      --database string
  -d, --dir string        The target dir
  -h, --help              help for ddl
      --home string       The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority
      --idea              For idea plugin [optional]
      --remote string     The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority
                          The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure
  -s, --src string        The path or path globbing patterns of the ddl
      --style string      The file naming format, see [https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md]


Global Flags:
  -i, --ignore-columns strings   Ignore columns while creating or updating rows (default [create_at,created_at,create_time,update_at,updated_at,update_time])
      --strict                   Generate model in strict mode
135yshr135yshr

datasource

octl model mysql datasource -h
Generate model from datasource

Usage:
  goctl model mysql datasource [flags]

Flags:
      --branch string   The branch of the remote repo, it does work with --remote
  -c, --cache           Generate code with cache [optional]
  -d, --dir string      The target dir
  -h, --help            help for datasource
      --home string     The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority
      --idea            For idea plugin [optional]
      --remote string   The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority
                        The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure
      --style string    The file naming format, see [https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md]
  -t, --table strings   The table or table globbing patterns in the database
      --url string      The data source of database,like "root:password@tcp(127.0.0.1:3306)/database


Global Flags:
  -i, --ignore-columns strings   Ignore columns while creating or updating rows (default [create_at,created_at,create_time,update_at,updated_at,update_time])
      --strict                   Generate model in strict mode
135yshr135yshr

キャッシュ

キャッシュについては、質問と回答の形式でリスト化しました。これにより、モデル内のキャッシュの機能をより明確に説明できると思います。

キャッシュは何の情報を保持しますか?

主キーフィールドのキャッシュでは、全体の構造情報がキャッシュされます。一方、単一のインデックスフィールド(全文インデックスを除く)では、主キーフィールドの値がキャッシュされます。

データの更新(update)操作はキャッシュをクリアしますか?

はい、ですが主キーのキャッシュ内の情報のみがクリアされます。その理由については、ここでは詳述しません。

なぜ単一のインデックスフィールドに基づいてupdateByXxxやdeleteByXxxのコードを生成しないのですか?

理論的には問題ありませんが、我々はモデル層のデータ操作は全体の構造、クエリも含めて基づいていると考えています。特定のフィールドだけをクエリすることは推奨していません(異論はないですが)、そうでなければ我々のキャッシュは無意味になります。

なぜfindPageLimitやfindAllのコード生成層をサポートしないのですか?

現在、基本的なCURD以外の他のコードはすべてビジネス向けのコードであると考えています。開発者がビジネスニーズに応じてコードを書く方がよいと考えています。

135yshr135yshr

データ型マッピング

mysql dataType golang dataType golang dataType(if null&&default null)
bool int64 sql.NullInt64
boolean int64 sql.NullInt64
tinyint int64 sql.NullInt64
smallint int64 sql.NullInt64
mediumint int64 sql.NullInt64
int int64 sql.NullInt64
integer int64 sql.NullInt64
bigint int64 sql.NullInt64
float float64 sql.NullFloat64
double float64 sql.NullFloat64
decimal float64 sql.NullFloat64
date time.Time sql.NullTime
datetime time.Time sql.NullTime
timestamp time.Time sql.NullTime
time string sql.NullString
year time.Time sql.NullInt64
char string sql.NullString
varchar string sql.NullString
binary string sql.NullString
varbinary string sql.NullString
tinytext string sql.NullString
text string sql.NullString
mediumtext string sql.NullString
longtext string sql.NullString
enum string sql.NullString
set string sql.NullString
json string sql.NullString

https://github.com/zeromicro/zero-doc/blob/main/go-zero.dev/en/goctl-model.md#type-conversion-rules

135yshr135yshr

簡単なSQLで検証

簡単なSQLを使って、どんなコード生成されるか検証する

準備

mkdir mall/user/rpc/model
cd mall/user/rpc/model

使うSQLはこんなの

mall/user/rpc/model/user.sql
CREATE TABLE Users (
    uuid CHAR(36) NOT NULL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    gender ENUM('M', 'F') NOT NULL,
    age INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

そして、ジェネレート!

goctl model mysql ddl -src user.sql -dir . -c

[convertColumns]: The column "uuid" is recommended to add constraint `DEFAULT`
[convertColumns]: The column "name" is recommended to add constraint `DEFAULT`
[convertColumns]: The column "gender" is recommended to add constraint `DEFAULT`
[convertColumns]: The column "age" is recommended to add constraint `DEFAULT`
Error: table Users: missing primary key
Usage:
  goctl model mysql ddl [flags]

Flags:
      --branch string     The branch of the remote repo, it does work with --remote
  -c, --cache             Generate code with cache [optional]
      --database string
  -d, --dir string        The target dir
  -h, --help              help for ddl
      --home string       The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority
      --idea              For idea plugin [optional]
      --remote string     The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority
                          The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure
  -s, --src string        The path or path globbing patterns of the ddl
      --style string      The file naming format, see [https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md]


Global Flags:
  -i, --ignore-columns strings   Ignore columns while creating or updating rows (default [create_at,created_at,create_time,update_at,updated_at,update_time])
      --strict                   Generate model in strict mode


table Users: missing primary key

失敗した。。。

135yshr135yshr

エラー対策

SQLを作り直して、改めてコード生成してみる
エラーの原因は、PRIMARY KEY句で始まる行がなかったからだった。
最初、Primary key に文字列指定してたからかと思ったけどそうでもなかった。

mall/user/model/user.sql
CREATE TABLE Users (
    uuid CHAR(36) NOT NULL,
    name VARCHAR(255) NOT NULL,
    gender ENUM('M', 'F') NOT NULL,
    age INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (uuid)
);

修正したSQLでコマンドを実行したら、うまくいった

goctl model mysql ddl -src user.sql -dir . -c
mall/user/rpc/model
├── user.sql
├── usersmodel.go
├── usersmodel_gen.go
└── vars.go
135yshr135yshr

生成結果

生成されたコード見ると基本的なCRUDのコードが生成されていた。
なお、ロック機構はないで使うときは呼び出し元で考慮すること。
基本的なCRUD以外は自分で実装すること

type usersModel interface {
	Insert(ctx context.Context, data *Users) (sql.Result, error)
	FindOne(ctx context.Context, uuid string) (*Users, error)
	Update(ctx context.Context, data *Users) error
	Delete(ctx context.Context, uuid string) error
}
135yshr135yshr

sql 変更

SQL変更したら、どんなことになるか確認
やるなら、フィールド追加なんだろうけど、そんなに問題になりそうにないので、primary key を変えてみる。

mall/user/rpc/model/user.sql
@@ -1,10 +1,11 @@
 CREATE TABLE Users (
+    id BIGINT NOT NULL AUTO_INCREMENT,
     uuid CHAR(36) NOT NULL,
     name VARCHAR(255) NOT NULL,
     gender ENUM('M', 'F') NOT NULL,
     age INT NOT NULL,
     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
     updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    PRIMARY KEY (uuid)
+    PRIMARY KEY (id)
 );

生成されたコードはこちら。

mall/user/rpc/model/usersmodel_gen.go
@@ -19,18 +19,18 @@ import (
 var (
        usersFieldNames          = builder.RawFieldNames(&Users{})
        usersRows                = strings.Join(usersFieldNames, ",")
-       usersRowsExpectAutoSet   = strings.Join(stringx.Remove(usersFieldNames, "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
-       usersRowsWithPlaceHolder = strings.Join(stringx.Remove(usersFieldNames, "`uuid`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"
+       usersRowsExpectAutoSet   = strings.Join(stringx.Remove(usersFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
+       usersRowsWithPlaceHolder = strings.Join(stringx.Remove(usersFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"

-       cacheUsersUuidPrefix = "cache:users:uuid:"
+       cacheUsersIdPrefix = "cache:users:id:"
 )

 type (
        usersModel interface {
                Insert(ctx context.Context, data *Users) (sql.Result, error)
-               FindOne(ctx context.Context, uuid string) (*Users, error)
+               FindOne(ctx context.Context, id int64) (*Users, error)
                Update(ctx context.Context, data *Users) error
-               Delete(ctx context.Context, uuid string) error
+               Delete(ctx context.Context, id int64) error
        }

        defaultUsersModel struct {
@@ -39,6 +39,7 @@ type (
        }

        Users struct {
+               Id        int64     `db:"id"`
                Uuid      string    `db:"uuid"`
                Name      string    `db:"name"`
                Gender    string    `db:"gender"`
@@ -55,21 +56,21 @@ func newUsersModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) *
        }
 }

-func (m *defaultUsersModel) Delete(ctx context.Context, uuid string) error {
-       usersUuidKey := fmt.Sprintf("%s%v", cacheUsersUuidPrefix, uuid)
+func (m *defaultUsersModel) Delete(ctx context.Context, id int64) error {
+       usersIdKey := fmt.Sprintf("%s%v", cacheUsersIdPrefix, id)
        _, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
-               query := fmt.Sprintf("delete from %s where `uuid` = ?", m.table)
-               return conn.ExecCtx(ctx, query, uuid)
-       }, usersUuidKey)
+               query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
+               return conn.ExecCtx(ctx, query, id)
+       }, usersIdKey)
        return err
 }

-func (m *defaultUsersModel) FindOne(ctx context.Context, uuid string) (*Users, error) {
-       usersUuidKey := fmt.Sprintf("%s%v", cacheUsersUuidPrefix, uuid)
+func (m *defaultUsersModel) FindOne(ctx context.Context, id int64) (*Users, error) {
+       usersIdKey := fmt.Sprintf("%s%v", cacheUsersIdPrefix, id)
        var resp Users
-       err := m.QueryRowCtx(ctx, &resp, usersUuidKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error {
-               query := fmt.Sprintf("select %s from %s where `uuid` = ? limit 1", usersRows, m.table)
-               return conn.QueryRowCtx(ctx, v, query, uuid)
+       err := m.QueryRowCtx(ctx, &resp, usersIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error {
+               query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", usersRows, m.table)
+               return conn.QueryRowCtx(ctx, v, query, id)
        })
        switch err {
        case nil:
@@ -82,29 +83,29 @@ func (m *defaultUsersModel) FindOne(ctx context.Context, uuid string) (*Users, e
 }

 func (m *defaultUsersModel) Insert(ctx context.Context, data *Users) (sql.Result, error) {
-       usersUuidKey := fmt.Sprintf("%s%v", cacheUsersUuidPrefix, data.Uuid)
+       usersIdKey := fmt.Sprintf("%s%v", cacheUsersIdPrefix, data.Id)
        ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
                query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?)", m.table, usersRowsExpectAutoSet)
                return conn.ExecCtx(ctx, query, data.Uuid, data.Name, data.Gender, data.Age)
-       }, usersUuidKey)
+       }, usersIdKey)
        return ret, err
 }

 func (m *defaultUsersModel) Update(ctx context.Context, data *Users) error {
-       usersUuidKey := fmt.Sprintf("%s%v", cacheUsersUuidPrefix, data.Uuid)
+       usersIdKey := fmt.Sprintf("%s%v", cacheUsersIdPrefix, data.Id)
        _, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
-               query := fmt.Sprintf("update %s set %s where `uuid` = ?", m.table, usersRowsWithPlaceHolder)
-               return conn.ExecCtx(ctx, query, data.Name, data.Gender, data.Age, data.Uuid)
-       }, usersUuidKey)
+               query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, usersRowsWithPlaceHolder)
+               return conn.ExecCtx(ctx, query, data.Uuid, data.Name, data.Gender, data.Age, data.Id)
+       }, usersIdKey)
        return err
 }

 func (m *defaultUsersModel) formatPrimary(primary any) string {
-       return fmt.Sprintf("%s%v", cacheUsersUuidPrefix, primary)
+       return fmt.Sprintf("%s%v", cacheUsersIdPrefix, primary)
 }

 func (m *defaultUsersModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary any) error {
-       query := fmt.Sprintf("select %s from %s where `uuid` = ? limit 1", usersRows, m.table)
+       query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", usersRows, m.table)
        return conn.QueryRowCtx(ctx, v, query, primary)
 }

うまく生成されたのでひと安心

135yshr135yshr

データベースからデータを取得

データベースから値を取得するロジックを新しく追加する。
作業に当たり、いくつか前提条件があるので、解決しておく。

前提条件

Redisは、キャッシュで利用するので立てておく必要がある

  1. MySQLサーバーの構築
    1. MySQLサーバーを起動
      • 今回は、ポートを 13306 に変更した
    2. mall データベースを作成
    3. user テーブルを作成
      • 使うDDLは、上のステップで作ったDDLを使う
  2. Redis サーバーの構築
    1. Redisサーバーを起動
      • 今回は、ポートを 16379 に変更した

Doker使うと便利

docker-compose.yaml
version: '3'

networks:
  etcd-network:
    driver: bridge
  db-network:
    driver: bridge

services:
  etcd:
    image: bitnami/etcd:3.5
    environment:
      - ALLOW_NONE_AUTHENTICATION=yes
      - ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379
    ports:
      - "12379:2379"
      - "12380:2380"
    networks:
      - etcd-network

  db:
    image: mysql:8.0
    volumes:
      - ./mysql/data:/var/lib/mysql
      - ./mysql/conf.d:/etc/mysql/conf.d
    environment:
      MYSQL_DATABASE: mall
      MYSQL_ROOT_PASSWORD: password
      MYSQL_USER: user
      MYSQL_PASSWOR: password
    restart: always
    ports:
      - "13306:3306"
    networks:
      - db-network

  redis:
    image: redis:7.0
    volumes:
      - ./redis/data:/data
    ports:
      - "16379:6379"
mysql/conf.d/my.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci

[client]
default-character-set=utf8mb4
135yshr135yshr

コード修正

大まかな流れは以下の通り。

  1. 設定ファイルにMySQLとRedisに接続する情報を追加する
  2. 設定ファイルに合わせて、 Config にデータベース接続文字列と CacheConf を追加する
  3. サービスコンテキストにユーザーモデルを追加する
  4. ビジネスロジックを変更して、データベースから値を取得する
mall/user/rpc/etc/user.yaml
@@ -4,3 +4,8 @@ Etcd:
   Hosts:
   - 127.0.0.1:12379
   Key: user.rpc
+DataSource: root:password@tcp(127.0.0.1:13306)/mall?parseTime=true
+Table: user
+Cache:
+  - Host: 127.0.0.1:16379
mall/user/rpc/internal/config/config.go
@@ -1,7 +1,12 @@
 package config

-import "github.com/zeromicro/go-zero/zrpc"
+import (
+       "github.com/zeromicro/go-zero/core/stores/cache"
+       "github.com/zeromicro/go-zero/zrpc"
+)

 type Config struct {
        zrpc.RpcServerConf
+       DataSource string
+       Cache      cache.CacheConf
 }
mall/user/rpc/internal/svc/servicecontext.go
@@ -1,13 +1,20 @@
 package svc

-import "github.com/SwipeVideo/go-zero-example/mall/user/rpc/internal/config"
+import (
+       "github.com/SwipeVideo/go-zero-example/mall/user/rpc/internal/config"
+       "github.com/SwipeVideo/go-zero-example/mall/user/rpc/model"
+
+       "github.com/zeromicro/go-zero/core/stores/sqlx"
+)

 type ServiceContext struct {
        Config config.Config
+       Model  model.UsersModel
 }

 func NewServiceContext(c config.Config) *ServiceContext {
        return &ServiceContext{
                Config: c,
+               Model:  model.NewUsersModel(sqlx.NewMysql(c.DataSource), c.Cache),
        }
 }
mall/user/rpc/internal/logic/getuserlogic.go
@@ -2,6 +2,7 @@ package logic

 import (
        "context"
+       "strconv"

        "github.com/SwipeVideo/go-zero-example/mall/user/rpc/internal/svc"
        "github.com/SwipeVideo/go-zero-example/mall/user/rpc/types/user"
@@ -24,10 +25,19 @@ func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLo
 }

 func (l *GetUserLogic) GetUser(in *user.IdRequest) (*user.UserResponse, error) {
+       id, err := strconv.ParseInt(in.Id, 10, 64)
+       if err != nil {
+               return nil, err
+       }
+
+       u, err := l.svcCtx.Model.FindOne(l.ctx, id)
+       if err != nil {
+               return nil, err
+       }
        return &user.UserResponse{
-               Id:     "1",
-               Name:   "test",
-               Gender: "male",
-               Age:    18,
+               Id:     strconv.FormatInt(u.Id, 10),
+               Name:   u.Name,
+               Gender: u.Gender,
+               Age:    int32(u.Age),
        }, nil
 }
135yshr135yshr

動作確認

修正が完了したので動作確認する
動作確認の流れは以下の通り

  1. etcd, MySQL, Redis サーバーを起動する
  2. ユーザー管理モジュールを起動する
  3. 注文管理モジュールを起動する
  4. 注文を取得するリクエストを投げる
135yshr135yshr

今回は、docker compose で裏方周りのサーバーを起動できるようにしたので利用する

docker compose up -d
135yshr135yshr

ユーザー管理と注文管理モジュールを起動する

go run ./mall/user/rpc/user.go -f ./mall/user/rpc/etc/user.yaml
go run ./order/api/order.go -f ./order/api/etc/order.yaml
135yshr135yshr

最後に注文を取得するリクエストを投げる

curl -i -X GET http://localhost:8888/api/order/get/1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Traceparent: 00-aa6d8f5002c6d1bf1dc2d8d9614defa7-52c5528f09ef2bf4-00
Date: Sat, 03 Jun 2023 13:37:24 GMT
Content-Length: 30

{"id":"1","name":"test order"}%
135yshr135yshr

書き忘れてしまっていたけど、事前にデータを入れておかないとエラーになる

INSERT INTO mall.Users
(id, uuid, name, gender, age)
VALUES(1, 'b914b453-4d75-4c69-a6a9-0ea2b04ff68e', 'test', 'M', 18);
135yshr135yshr

model 使ってみた感想

いいところ

  1. sql 書いてコマンド実行するとモデル作ってくれる
  2. 基本的なCRUDを生成してくれるから使うだけ
  3. キャッシュ化もまとめてやってくれる

いまいちなところ

  1. sql コードを直接書き換える必要があり、ロールバックの仕組みを用意する必要ある
  2. 基本的なCRUDは作ってくれるけど、それ以外はがんばって実装
    • 全部とは言わなくてもテンプレくらいあるといいなぁ