go-zeroを学ぶ2
前回は、Getting Started をやってまとめた。
今回は、Getting Started でやっていなかった機能を触ってみる
Model サブコマンドから始める
データベースを使う要件はよくあると思われるので、今回は、model をまとめてみる。
データベース使うときによく話にあがってくるのが、この辺なのでどのくらい楽になるか調査してみようとおもう
- migration どうしよう
- バージョン管理どうしよう
- テーブルとモデルの乖離どうしよう
- とかとか
model サブコマンド
model サブコマンドは、モデル層のコードを生成するためのコマンドらしい。
MySQLのDDL読み込んでコード生成できたり、データベースに接続して、モデルコード作成もやってくれたり、至れり尽くせり。(要検証だけど)
対応するデータベース
- MySQL(mysql)
- MongoDB(mongdb)
- PostgreSQL(pg)
goctl model
の後に、mysql
や mongo
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つのフィールドが必要ない場合は問題ありません。
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
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
キャッシュ
キャッシュについては、質問と回答の形式でリスト化しました。これにより、モデル内のキャッシュの機能をより明確に説明できると思います。
キャッシュは何の情報を保持しますか?
主キーフィールドのキャッシュでは、全体の構造情報がキャッシュされます。一方、単一のインデックスフィールド(全文インデックスを除く)では、主キーフィールドの値がキャッシュされます。
データの更新(update)操作はキャッシュをクリアしますか?
はい、ですが主キーのキャッシュ内の情報のみがクリアされます。その理由については、ここでは詳述しません。
なぜ単一のインデックスフィールドに基づいてupdateByXxxやdeleteByXxxのコードを生成しないのですか?
理論的には問題ありませんが、我々はモデル層のデータ操作は全体の構造、クエリも含めて基づいていると考えています。特定のフィールドだけをクエリすることは推奨していません(異論はないですが)、そうでなければ我々のキャッシュは無意味になります。
なぜfindPageLimitやfindAllのコード生成層をサポートしないのですか?
現在、基本的なCURD以外の他のコードはすべてビジネス向けのコードであると考えています。開発者がビジネスニーズに応じてコードを書く方がよいと考えています。
データ型マッピング
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 |
簡単なSQLで検証
簡単なSQLを使って、どんなコード生成されるか検証する
準備
mkdir mall/user/rpc/model
cd mall/user/rpc/model
使う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
失敗した。。。
エラー対策
SQLを作り直して、改めてコード生成してみる
エラーの原因は、PRIMARY KEY句で始まる行がなかったからだった。
最初、Primary key に文字列指定してたからかと思ったけどそうでもなかった。
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
生成結果
生成されたコード見ると基本的な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
}
sql 変更
SQL変更したら、どんなことになるか確認
やるなら、フィールド追加なんだろうけど、そんなに問題になりそうにないので、primary key
を変えてみる。
@@ -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)
);
生成されたコードはこちら。
@@ -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)
}
うまく生成されたのでひと安心
データベースからデータを取得
データベースから値を取得するロジックを新しく追加する。
作業に当たり、いくつか前提条件があるので、解決しておく。
前提条件
Redisは、キャッシュで利用するので立てておく必要がある
- MySQLサーバーの構築
- MySQLサーバーを起動
- 今回は、ポートを
13306
に変更した
- 今回は、ポートを
-
mall
データベースを作成 -
user
テーブルを作成- 使うDDLは、上のステップで作ったDDLを使う
- MySQLサーバーを起動
- Redis サーバーの構築
- Redisサーバーを起動
- 今回は、ポートを
16379
に変更した
- 今回は、ポートを
- Redisサーバーを起動
Doker使うと便利
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"
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
[client]
default-character-set=utf8mb4
コード修正
大まかな流れは以下の通り。
- 設定ファイルにMySQLとRedisに接続する情報を追加する
- 設定ファイルに合わせて、
Config
にデータベース接続文字列とCacheConf
を追加する - サービスコンテキストにユーザーモデルを追加する
- ビジネスロジックを変更して、データベースから値を取得する
@@ -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
@@ -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
}
@@ -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),
}
}
@@ -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
}
動作確認
修正が完了したので動作確認する
動作確認の流れは以下の通り
- etcd, MySQL, Redis サーバーを起動する
- ユーザー管理モジュールを起動する
- 注文管理モジュールを起動する
- 注文を取得するリクエストを投げる
今回は、docker compose
で裏方周りのサーバーを起動できるようにしたので利用する
docker compose up -d
ユーザー管理と注文管理モジュールを起動する
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
最後に注文を取得するリクエストを投げる
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"}%
書き忘れてしまっていたけど、事前にデータを入れておかないとエラーになる
INSERT INTO mall.Users
(id, uuid, name, gender, age)
VALUES(1, 'b914b453-4d75-4c69-a6a9-0ea2b04ff68e', 'test', 'M', 18);
model 使ってみた感想
いいところ
- sql 書いてコマンド実行するとモデル作ってくれる
- 基本的なCRUDを生成してくれるから使うだけ
- キャッシュ化もまとめてやってくれる
いまいちなところ
- sql コードを直接書き換える必要があり、ロールバックの仕組みを用意する必要ある
- 基本的なCRUDは作ってくれるけど、それ以外はがんばって実装
- 全部とは言わなくてもテンプレくらいあるといいなぁ