在使用 Go-zero 进行开发时,
goctl
工具可以为我们自动生成 Model 层代码,大大提高了开发效率。本文将详细介绍如何使用goctl model mysql datasource
和goctl model mysql ddl
两种方式来生成 MySQL 的 Model 层代码,并展示如何在 Go-zero 项目中使用生成的 Model 代码。
1. 使用 goctl model mysql datasource
指令生成 Model
goctl model mysql datasource
指令用于从数据库连接生成 Model 代码。通过该指令,我们可以直接连接到 MySQL 数据库,并根据指定的数据表生成对应的 Golang 结构体、CURD 操作方法以及缓存逻辑等。
使用示例
假设我们要从本地的 MySQL 数据库(用户名为 user
,密码为 yourpwd
,地址为 127.0.0.1:3306
,数据库名为 yourdatabase
)中生成 user
表和 topic
表的 Model 代码,并且生成带缓存逻辑的代码。我们可以使用以下命令:
goctl model mysql datasource -url="user:yourpwd@tcp(127.0.0.1:3306)/yourdatabase" -t=user,topic -dir ./internal/model -c -style goZero
参数说明
branch
: 远程模板所在 git 分支名称,仅当remote
有值时使用。cache
: 是否生成带缓存的代码。dir
: 代码输出目录。easy
: 是否暴露集合名称变量。home
: 本地模板文件目录。idea
: 为 idea 插件服务。remote
: 远程模板所在 git 仓库地址,当此字段传值时,优先级高于home
字段值。style
: 输出文件和目录的命名风格格式化符号。table
: 需要生成代码的表。url
: 数据库连接,格式{{username}}:{{password}}@tcp({{host_port}})/{{db}}
。ignore-columns
: 需要忽略的字段。prefix
: 自定义缓存 key 前缀。strict
: 是否是严格模式。
2. 使用 goctl model mysql ddl
指令生成 Model
goctl model mysql ddl
指令用于从 SQL 文件生成 Model 代码。这种方式特别适合在项目开始时,已经定义好了数据库结构的 SQL 文件,或者在迁移项目时,需要基于现有 SQL 文件生成 Model。
使用示例
假设我们有一个名为 user.sql
的文件,里面包含了创建 user
表的 SQL 语句,现在我们想根据这个文件来生成 Model 代码。我们可以使用以下命令:
v1/model/user.sql
CREATE TABLE user
(
id bigint AUTO_INCREMENT,
username varchar(36) NOT NULL,
password varchar(64) default '',
UNIQUE name_index (username),
PRIMARY KEY (id)
) ENGINE = InnoDB COLLATE utf8mb4_general_ci;
生成 Go 代码:
goctl model mysql ddl --src user.sql --dir .
生成的 Go 代码自动为我们生成了增删改查的代码。
生成的代码使用
在配置文件中写上 MySQL 配置:
// v1/api/internal/config/config.go
package config
import "github.com/zeromicro/go-zero/rest"
type Config struct {
rest.RestConf
Mysql struct {
DataSource string
}
Auth struct {
AccessSecret string
AccessExpire int64
}
}
配置文件示例:
Name: users
Host: 0.0.0.0
Port: 8888
Mysql:
DataSource: root:root@tcp(127.0.0.1:3306)/zero_db?charset=utf8mb4&parseTime=True&loc=Local
Auth:
AccessSecret: dfff1234
AccessExpire: 3600
在依赖注入的地方创建连接:
// v1/api/internal/svc/servicecontext.go
package svc
import (
"github.com/zeromicro/go-zero/core/stores/sqlx"
"go_test/v1/api/internal/config"
"go_test/v1/model"
)
type ServiceContext struct {
Config config.Config
UsersModel model.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
mysqlConn := sqlx.NewMysql(c.Mysql.DataSource)
return &ServiceContext{
Config: c,
UsersModel: model.NewUserModel(mysqlConn),
}
}
在登录逻辑中使用生成的 Model:
// v1/api/internal/logic/loginlogic.go
package logic
import (
"context"
"fmt"
"errors"
"go_test/v1/api/internal/svc"
"go_test/v1/api/internal/types"
"go_test/v1/model"
)
type LoginLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
return &LoginLogic{
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *LoginLogic) Login(req *types.LoginRequest) (resp string, err error) {
// 增
_, err = l.svcCtx.UsersModel.Insert(l.ctx, &model.User{
Username: "枫枫",
Password: "123456",
})
if err != nil {
return "", err
}
// 查
user, err := l.svcCtx.UsersModel.FindOne(l.ctx, 1)
fmt.Println(user, err)
// 查
user, err = l.svcCtx.UsersModel.FindOneByUsername(l.ctx, "枫枫")
fmt.Println(user, err)
// 改
err = l.svcCtx.UsersModel.Update(l.ctx, &model.User{
Username: "枫枫1",
Password: "1234567",
Id: 1,
})
user, err = l.svcCtx.UsersModel.FindOne(l.ctx, 1)
fmt.Println(user, err)
// 删
err = l.svcCtx.UsersModel.Delete(l.ctx, 1)
user, err = l.svcCtx.UsersModel.FindOne(l.ctx, 1)
fmt.Println(user, err)
return "登录成功", nil
}
3. go-zero 原生操作 MySQL
上述示例展示了如何使用 goctl
工具生成的 Model 进行增删改查操作。Go-zero 提供了原生的 SQL 操作方式,让我们可以直接使用 SQL 进行数据库操作。
代码生成
goctl model mysql ddl --src user.sql --dir .
代码使用
在配置文件中写上 MySQL 配置(如上所示)。
在依赖注入的地方创建连接(如上所示)。
在业务逻辑中使用生成的 Model(如上所示)。
4. 结合 GORM 使用 MySQL方式
虽然 Go-zero 提供了强大的原生 SQL 操作能力,但在实际开发中,结合 GORM 等 ORM 工具会更加高效。
直接编写 Model 文件
// v1/model/usermodel.go
package model
import "gorm.io/gorm"
type User struct {
gorm.Model
Username string `gorm:"size:36" json:"username"`
Password string `gorm:"size:64" json:"password"`
}
初始化 GORM 连接
// common/init_db/init_gorm.go
package init_db
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// InitGorm 初始化 GORM 连接
func InitGorm(MysqlDataSource string) *gorm.DB {
db, err := gorm.Open(mysql.Open(MysqlDataSource), &gorm.Config{})
if err != nil {
panic("连接 MySQL 数据库失败, error=" + err.Error())
} else {
fmt.Println("连接 MySQL 数据库成功")
}
return db
}
在 Context 中注入 GORM 连接
// v1/api/internal/svc/servicecontext.go
package svc
import (
"go_test/common/init_db"
"go_test/v1/api/internal/config"
"go_test/v1/model"
"gorm.io/gorm"
)
type ServiceContext struct {
Config config.Config
DB *gorm.DB
}
func NewServiceContext(c config.Config) *ServiceContext {
mysqlDb := init_db.InitGorm(c.Mysql.DataSource)
mysqlDb.AutoMigrate(&model.User{})
return &ServiceContext{
Config: c,
DB: mysqlDb,
}
}
使用 GORM 进行数据库操作
// v1/api/internal/logic/loginlogic.go
package logic
import (
"context"
"errors"
"go_test/v1/api/internal/svc"
"go_test/v1/api/internal/types"
"go_test/v1/model"
)
type LoginLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
return &LoginLogic{
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *LoginLogic) Login(req *types.LoginRequest) (resp string, err error) {
var user model.User
err = l.svcCtx.DB.Where("username = ? and password = ?", req.Username, req.Password).First(&user).Error
if err != nil {
return "", errors.New("登录失败")
}
return user.Username, nil
}
多表查询的使用方式
上面go-zero自动生成的代码,仅是单表查询。但现实使用中,更常见的场景是多表级联查询。以下展示多表查询的使用例子。以查询user表和topic表,获取博客作者排行榜的业务场景举例,使用如下:
package model
import (
"context"
"fmt"
"time"
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
var _ TopicModel = (*customTopicModel)(nil)
type (
// TopicModel is an interface to be customized, add more methods here,
// and implement the added methods in customTopicModel.
TopicModel interface {
topicModel
GetUserRankingWithCache(ctx context.Context, limit int64) ([]UserRankingItem, error)
}
customTopicModel struct {
*defaultTopicModel
conn sqlx.SqlConn
}
)
// UserRankingItem 用户排行榜
type UserRankingItem struct {
UserName string `db:"userName" json:"userName"`
Nickname string `db:"nickname" json:"nickname"`
AvatarName string `db:"avatarName" json:"avatarName"`
AvatarUrl string `db:"avatar_url" json:"avatarUrl"`
ArticleCount int64 `db:"article_count" json:"articleCount"`
TotalViews int64 `db:"total_views" json:"totalViews"`
TotalComments int64 `db:"total_comments" json:"totalComments"`
LastPostTime time.Time `db:"last_post_time" json:"lastPostTime"`
Score int64 `db:"score" json:"score"`
}
// NewTopicModel returns a model for the database table.
func NewTopicModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) TopicModel {
return &customTopicModel{
defaultTopicModel: newTopicModel(conn, c, opts...),
conn: conn,
}
}
// GetUserRankingByArticleCount 根据文章数量获取用户排行榜
func (m *customTopicModel) GetUserRankingByArticleCount(ctx context.Context, limit int64) ([]UserRankingItem, error) {
query := `
SELECT
u.userName,
u.nickname,
u.avatarName,
CASE
WHEN u.avatarName IS NULL OR u.avatarName = ''
THEN CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/default-avatar.png')
ELSE CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/avatar/', u.avatarName)
END AS avatar_url,
COUNT(t.id) AS article_count,
SUM(t.viewTotal) AS total_views,
SUM(t.commentTotal) AS total_comments,
MAX(t.postTime) AS last_post_time,
COUNT(t.id) AS score
FROM user u
LEFT JOIN topic t ON u.userName = t.userName
WHERE t.status = 20
AND t.allow = 1
GROUP BY u.userName, u.nickname, u.avatarName
HAVING COUNT(t.id) >= 1
ORDER BY COUNT(t.id) DESC, SUM(t.viewTotal) DESC
LIMIT ?
`
var result []UserRankingItem
err := m.conn.QueryRowsCtx(ctx, &result, query, limit)
return result, err
}
// GetUserRankingByViews 根据浏览量获取用户排行榜
func (m *customTopicModel) GetUserRankingByViews(ctx context.Context, limit int64) ([]UserRankingItem, error) {
query := `
SELECT
u.userName,
u.nickname,
u.avatarName,
CASE
WHEN u.avatarName IS NULL OR u.avatarName = ''
THEN CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/default-avatar.png')
ELSE CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/avatar/', u.avatarName)
END AS avatar_url,
COUNT(t.id) AS article_count,
SUM(t.viewTotal) AS total_views,
SUM(t.commentTotal) AS total_comments,
MAX(t.postTime) AS last_post_time,
SUM(t.viewTotal) AS score
FROM user u
LEFT JOIN topic t ON u.userName = t.userName
WHERE t.status = 20
AND t.allow = 1
GROUP BY u.userName, u.nickname, u.avatarName
HAVING COUNT(t.id) >= 1
ORDER BY SUM(t.viewTotal) DESC, COUNT(t.id) DESC
LIMIT ?
`
var result []UserRankingItem
err := m.conn.QueryRowsCtx(ctx, &result, query, limit)
return result, err
}
// GetUserRankingByComprehensiveScore 根据综合评分获取用户排行榜
func (m *customTopicModel) GetUserRankingByComprehensiveScore(ctx context.Context, limit int64) ([]UserRankingItem, error) {
query := `
SELECT
u.userName,
u.nickname,
u.avatarName,
CASE
WHEN u.avatarName IS NULL OR u.avatarName = ''
THEN CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/default-avatar.png')
ELSE CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/avatar/', u.avatarName)
END AS avatar_url,
COUNT(t.id) AS article_count,
SUM(t.viewTotal) AS total_views,
SUM(t.commentTotal) AS total_comments,
MAX(t.postTime) AS last_post_time,
(COUNT(t.id) * 10 + SUM(t.viewTotal) + SUM(t.commentTotal) * 5) AS score
FROM user u
LEFT JOIN topic t ON u.userName = t.userName
WHERE t.status = 20
AND t.allow = 1
GROUP BY u.userName, u.nickname, u.avatarName
HAVING COUNT(t.id) >= 1
ORDER BY score DESC
LIMIT ?
`
var result []UserRankingItem
err := m.conn.QueryRowsCtx(ctx, &result, query, limit)
return result, err
}
// GetUserRankingWithQuestion 包含问答内容的综合排行榜
func (m *customTopicModel) GetUserRankingWithQuestion(ctx context.Context, limit int64) ([]UserRankingItem, error) {
query := `
SELECT
u.userName,
u.nickname,
u.avatarName,
CASE
WHEN u.avatarName IS NULL OR u.avatarName = ''
THEN CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/default-avatar.png')
ELSE CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/avatar/', u.avatarName)
END AS avatar_url,
COUNT(t.id) AS article_count,
COUNT(q.id) AS question_count,
SUM(t.viewTotal) AS total_views,
SUM(t.commentTotal) AS total_comments,
MAX(t.postTime) AS last_post_time,
(COUNT(t.id) + COUNT(q.id)) AS score
FROM user u
LEFT JOIN topic t ON u.userName = t.userName AND t.status = 20 AND t.allow = 1
LEFT JOIN question q ON u.userName = q.userName AND q.status = 20 AND q.allow = 1
GROUP BY u.userName, u.nickname, u.avatarName
HAVING (COUNT(t.id) + COUNT(q.id)) >= 1
ORDER BY (COUNT(t.id) + COUNT(q.id)) DESC, SUM(t.viewTotal) DESC
LIMIT ?
`
var result []UserRankingItem
err := m.conn.QueryRowsCtx(ctx, &result, query, limit)
return result, err
}
// GetUserRankingByTimeRange 根据时间范围获取用户排行榜
func (m *customTopicModel) GetUserRankingByTimeRange(ctx context.Context, startTime, endTime time.Time, limit int64) ([]UserRankingItem, error) {
query := `
SELECT
u.userName,
u.nickname,
IFNULL(u.avatarName, '') AS avatarName,
CASE
WHEN u.avatarName IS NULL OR u.avatarName = ''
THEN CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/default-avatar.png')
ELSE CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/avatar/', u.avatarName)
END AS avatar_url,
COUNT(t.id) AS article_count,
SUM(t.viewTotal) AS total_views,
SUM(t.commentTotal) AS total_comments,
MAX(t.postTime) AS last_post_time,
COUNT(t.id) AS score
FROM user u
LEFT JOIN topic t ON u.userName = t.userName
WHERE t.status = 20
AND t.allow = 1
AND t.postTime BETWEEN ? AND ?
GROUP BY u.userName, u.nickname, u.avatarName
HAVING COUNT(t.id) >= 1
ORDER BY COUNT(t.id) DESC, SUM(t.viewTotal) DESC
LIMIT ?
`
var result []UserRankingItem
err := m.conn.QueryRowsCtx(ctx, &result, query, startTime, endTime, limit)
return result, err
}
// GetUserRankingWithCache 带缓存的用户排行榜
func (m *customTopicModel) GetUserRankingWithCache(ctx context.Context, limit int64) ([]UserRankingItem, error) {
cacheKey := fmt.Sprintf("user_ranking:%d", limit)
// 尝试从缓存获取
var result []UserRankingItem
err := m.CachedConn.QueryRowCtx(ctx, &result, cacheKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error {
fmt.Println("GetUserRankingWithCache from mysql:")
query := `
SELECT
u.userName,
u.nickname,
IFNULL(u.avatarName, '') AS avatarName,
CASE
WHEN u.avatarName IS NULL OR u.avatarName = ''
THEN CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/default-avatar.png')
ELSE CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/avatar/', u.avatarName)
END AS avatar_url,
COUNT(t.id) AS article_count,
SUM(t.viewTotal) AS total_views,
SUM(t.commentTotal) AS total_comments,
MAX(t.postTime) AS last_post_time,
COUNT(t.id) AS score
FROM user u
LEFT JOIN topic t ON u.userName = t.userName
WHERE t.status = 20
AND t.allow = 1
GROUP BY u.userName, u.nickname, u.avatarName
HAVING COUNT(t.id) >= 1
ORDER BY COUNT(t.id) DESC, SUM(t.viewTotal) DESC
LIMIT ?
`
// 使用传入的conn执行查询
return conn.QueryRowsCtx(ctx, v, query, limit)
})
if err != nil {
fmt.Printf("error GetUserRankingWithCache:%s", err.Error())
return result, nil
}
fmt.Println("GetUserRankingWithCache ok")
// 设置缓存(这里需要根据你的缓存实现来调整)
m.CachedConn.SetCache(cacheKey, result)
return result, nil
}
上述注意,需要正确的处理NULL值。因为type struct定义的排行榜结构体中的avatarName为string类型,而查询出来的结果可能为NULL,如果不处理,则会报错:Scan error on column index 2, name “avatarName”: converting NULL to string is unsupported
解决办法是sql语句中,如下处理:
要让 avatarName 不返回 NULL,可以在 SQL 中使用 COALESCE 或 IFNULL 函数来处理。
以下是几种改写方式:
query := `
SELECT
u.userName,
u.nickname,
COALESCE(u.avatarName, '') AS avatarName,
CASE
WHEN u.avatarName IS NULL OR u.avatarName = ''
THEN CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/default-avatar.png')
ELSE CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/avatar/', u.avatarName)
END AS avatar_url,
COUNT(t.id) AS article_count,
SUM(t.viewTotal) AS total_views,
SUM(t.commentTotal) AS total_comments,
MAX(t.postTime) AS last_post_time,
COUNT(t.id) AS score
FROM user u
LEFT JOIN topic t ON u.userName = t.userName
WHERE t.status = 20
AND t.allow = 1
GROUP BY u.userName, u.nickname, u.avatarName
HAVING COUNT(t.id) >= 1
ORDER BY COUNT(t.id) DESC, SUM(t.viewTotal) DESC
LIMIT ?
`
或者可以这样:
query := `
SELECT
u.userName,
u.nickname,
IFNULL(u.avatarName, '') AS avatarName,
CASE
WHEN u.avatarName IS NULL OR u.avatarName = ''
THEN CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/default-avatar.png')
ELSE CONCAT('https://your-bucket.oss-cn-region.aliyuncs.com/avatar/', u.avatarName)
END AS avatar_url,
COUNT(t.id) AS article_count,
SUM(t.viewTotal) AS total_views,
SUM(t.commentTotal) AS total_comments,
MAX(t.postTime) AS last_post_time,
COUNT(t.id) AS score
FROM user u
LEFT JOIN topic t ON u.userName = t.userName
WHERE t.status = 20
AND t.allow = 1
GROUP BY u.userName, u.nickname, u.avatarName
HAVING COUNT(t.id) >= 1
ORDER BY COUNT(t.id) DESC, SUM(t.viewTotal) DESC
LIMIT ?
`
总结
通过上述内容,我们不仅了解了如何使用 goctl
工具生成 MySQL Model 代码,还学会了如何在 Go-zero 项目中直接使用这些 Model 进行数据库操作,以及如何结合 GORM 进行更高效的开发。希望这些内容能帮助大家更好地理解和使用 Go-zero 进行 MySQL 开发。
参考文档
- Go-zero 学习 - 第四章 数据库操作(MySQL)
- https://go-zero.dev/docs/tutorials
- https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md
- https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config