gozero 的 goctl 工具生成 MySQL Model 层代码的两种方式及使用介绍

在使用 Go-zero 进行开发时,goctl 工具可以为我们自动生成 Model 层代码,大大提高了开发效率。本文将详细介绍如何使用 goctl model mysql datasourcegoctl 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 开发。

参考文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

特立独行的猫a

您的鼓励是我的创作动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值