golang学习笔记——gorm


gen是gorm官方推出的一个GORM代码生成工具

官方文档:https://gorm.io/zh_CN/gen/

1.使用gen框架生成model和dao

安装gorm gen

1
go get -u gorm.io/gen

假设有如下用户表

1
2
3
4
5
6
7
CREATE TABLE user
(
    `id`     bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
    `username`  varchar(128) NOT NULL COMMENT '用户名',
    `email` varchar(128) NOT NULL COMMENT '邮箱',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

在cmd目录下创建gen/generate.go文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main
 
import (
    "fmt"
 
    "gorm.io/driver/mysql"
    "gorm.io/gen"
    "gorm.io/gorm"
)
 
const MySQLDSN = "root:123456@tcp(127.0.0.1:55000)/default?charset=utf8mb4&parseTime=True"
 
func connectDB(dsn string) *gorm.DB {
    db, err := gorm.Open(mysql.Open(dsn))
    if err != nil {
        panic(fmt.Errorf("connect mysql fail: %w", err))
    }
    return db
}
 
func main() {
    // 指定生成代码的具体相对目录(相对当前文件),默认为:./query
    // 默认生成需要使用WithContext之后才可以查询的代码,但可以通过设置gen.WithoutContext禁用该模式
    g := gen.NewGenerator(gen.Config{
        // 默认会在 OutPath 目录生成CRUD代码,并且同目录下生成 model 包
        // 所以OutPath最终package不能设置为model,在有数据库表同步的情况下会产生冲突
        // 若一定要使用可以通过ModelPkgPath单独指定model package的名称
        OutPath:      "./internal/dao",
        ModelPkgPath: "./internal/model",
 
        // gen.WithoutContext:禁用WithContext模式
        // gen.WithDefaultQuery:生成一个全局Query对象Q
        // gen.WithQueryInterface:生成Query接口
        Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface,
    })
 
    // 通常复用项目中已有的SQL连接配置db(*gorm.DB)
    // 非必需,但如果需要复用连接时的gorm.Config或需要连接数据库同步表信息则必须设置
    g.UseDB(connectDB(MySQLDSN))
 
    // 从连接的数据库为所有表生成Model结构体和CRUD代码
    //g.ApplyBasic(g.GenerateAllTable()...)
 
    // 也可以手动指定需要生成代码的数据表
    //g.ApplyBasic(g.GenerateModel("user"), g.GenerateModel("role"))
 
    // 还可以指定只生成model
    var models = [...]string{"user"}
    for _, tableName := range models {
        // 只生成model
        g.GenerateModel(tableName)
        // 生成model和query
        //tableModel := g.GenerateModel(tableName)
        //g.ApplyBasic(tableModel)
    }
 
    //g.ApplyInterface(func(model.Filter) {}, g.GenerateModel("user"))
    // 执行并生成代码
    g.Execute()
}

运行generate.go,将会生成model和dao文件

需要注意mysql的tinyint(1)生成的时候会映射成bool,但是tinyint(1)的实际范围为-128~127,这可能会有些问题;而tinyint(4)生成的时候会映射成int32

2.gorm框架CRUD

1.insert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func init() {
    SetDefault(database.DB)
}
 
func Test_userDo_Create(t *testing.T) {
    user := model.User{
        Username: "test",
        Email:    "test@test",
    }
    err := User.Create(&user)
    if err != nil {
        fmt.Println("create user fail")
    }
}

2.delete

1
2
3
4
5
6
7
8
9
10
11
12
13
func Test_userDo_Delete(t *testing.T) {
    /*
        user := model.User{
            ID: 1,
        }
        result, err := User.Delete(&user)
    */
    result, err := User.Where(User.ID.Eq(2)).Delete()
    if err != nil {
        fmt.Println("delete user fail")
    }
    fmt.Println(result)
}

3.update

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Test_userDo_Update(t *testing.T) {
    /*
        user := model.User{
            ID:       2,
            Username: "test2",
            Email:    "test2@test",
        }
        result, err := User.Updates(&user)
    */
    result, err := User.
        Where(User.ID.Eq(2)).
        Update(User.Username, "test22")
 
    if err != nil {
        fmt.Println("update user fail")
    }
    fmt.Println(result.RowsAffected)
}

4.select

1
2
3
4
5
6
7
8
func Test_userDo_Scan(t *testing.T) {
    user := model.User{}
    err := User.Where(User.ID.Eq(2)).Scan(&user)
    if err != nil {
        fmt.Println("scan user fail")
    }
    fmt.Println(user)
}

参考:GORM Gen使用指南

5.Gorm处理可变结果集

在上面例子中使用scan获得查询结果的时候,字段的个数是固定的,如果当字段的个数是不定长度的时候,可以使用gorm来处理可变结果集

可以将[]interface{}或者[]orm.Params来保存查询结果,参考:Golang开发实践:把数据库数据保存到map[string]interface{}中 或者【巧妙】GO + MySQL的通用查询方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
func (repo *userRepo) ListUsers(ctx context.Context) (*model.User, error) {
    connect, err := repo.data.db.DB()
    if err != nil {
        repo.log.Error(fmt.Sprintf("connect fail, error: %v", err))
        return nil, err
    }
    rows, err := connect.Query("select * from `user`")
    if err != nil {
        repo.log.Error(fmt.Sprintf("query fail, error: %v", err))
        return nil, err
    }
    defer rows.Close()
    cols, err := rows.Columns()
    if err != nil {
        return nil, err
    }
    values := make([]interface{}, 0)
    for i := 0; i < len(cols); i++ {
        var value interface{}
        values = append(values, &value)
    }
    for rows.Next() {
        err = rows.Scan(values...)
        if err != nil {
            repo.log.Error(fmt.Sprintf("scan fail, error: %v", err))
            return nil, err
        }
        for k, v := range values {
            key := cols[k]
            var rawValue = *(v.(*interface{}))
            switch v := rawValue.(type) {
            case string:
                fmt.Print("key=>" + key + ":string ")
                fmt.Print(v)
            case int32:
                fmt.Print("key=>" + key + ":int32 ")
                fmt.Println(v)
            case []uint8:
                fmt.Print("key=>" + key + ",type=>uint8[],value=>")
                fmt.Print(string(v))
                fmt.Print(" ")
            default:
                fmt.Print(v)
            }
        }
        fmt.Println()
    }
    return nil, nil
}

输出结果

1
2
3
4
5
key=>id,type=>uint8[],value=>5 key=>username,type=>uint8[],value=>test key=>email,type=>uint8[],value=>test@test
key=>id,type=>uint8[],value=>6 key=>username,type=>uint8[],value=>test key=>email,type=>uint8[],value=>test@test
key=>id,type=>uint8[],value=>7 key=>username,type=>uint8[],value=>test key=>email,type=>uint8[],value=>test@test
key=>id,type=>uint8[],value=>8 key=>username,type=>uint8[],value=>test key=>email,type=>uint8[],value=>test@test
key=>id,type=>uint8[],value=>9 key=>username,type=>uint8[],value=>test key=>email,type=>uint8[],value=>test@test

6.分页

可以使用gorm的scopes来实现分页(先count再分页),参考:Gorm Scopes复用你的逻辑学习gorm系列十之:使用gorm.Scopes函数复用你的查询逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func Paginate(pageNum int, pageSize int) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        if pageNum <= 0 {
            pageNum = 1
        }
        if pageSize > 100 {
            pageSize = 100
        } else if pageSize <= 0 {
            pageSize = 10
        }
        offset := (pageNum - 1) * pageSize
        return db.Offset(offset).Limit(pageSize)
    }
}
 
func (repo *userRepo) ListUser(ctx context.Context, pageNum int, pageSize int) ([]*model.User, error) {
    var result []*model.User
    var total int64
    err := repo.data.db.Model(&model.User{}).Count(&total).Error
    if err != nil {
        repo.log.Error(fmt.Sprintf("list user fail, error: %v", err))
    }
    err = repo.data.db.Scopes(Paginate(pageNum, pageSize)).Find(&result).Error
    if err != nil {
        repo.log.Error(fmt.Sprintf("update user fail, error: %v", err))
        return nil, err
    }
    return result, nil
}

测试分页函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func Test_userRepo_ListUsers(t *testing.T) {
    data, _, err := NewData(zap_logger, db, rdb)
    if err != nil {
        panic(err)
    }
    userRepo := userRepo{data: data, log: zap_logger}
    result, err := userRepo.ListUser(context.Background(), 1, 5)
    if err != nil {
        fmt.Println(err)
    }
    for _, user := range result {
        fmt.Println(user)
    }
}

输出

1
2
3
4
5
&{5 test test@test}
&{6 test test@test}
&{7 test test@test}
&{8 test test@test}
&{9 test test@test}

Mybatis Page Helper分页插件原理:Mybatis分页插件PageHelper的配置和使用方法

3.gorm框架自定义SQL

gorm还支持编写sql模板,来添加自定义sql的函数逻辑,其中使用的语法是text template,可以参考: go学习笔记——text template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package model
 
import "gorm.io/gen"
 
type Filter interface {
    // FilterWithColumn SELECT * FROM @@table WHERE @@column=@value
    FilterWithColumn(column string, value string) ([]*gen.T, error)
 
    // FilterWithObject
    //
    // SELECT * FROM @@table where id > 0
    // {{if user != nil}}
    //   {{if user.ID > 0}}
    //     AND id = @user.ID
    //   {{else if user.Username != ""}}
    //     AND username=@user.Username
    //   {{else if user.Email != ""}}
    //     AND email=@user.Email
    //   {{end}}
    // {{end}}
    FilterWithObject(user *gen.T) ([]*gen.T, error)
}

然后在generate.go文件中添加

1
g.ApplyInterface(func(model.Filter) {}, g.GenerateModel("user"))

使用自定义SQL生成的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func Test_userDo_FilterWithColumn(t *testing.T) {
    result, err := User.FilterWithColumn("username", "test")
    if err != nil {
        fmt.Println("filter user fail")
    }
    for _, each := range result {
        fmt.Println(each)
    }
}
 
func Test_userDo_FilterWithObject(t *testing.T) {
    user := model.User{
        ID:       3,
        Username: "test2",
        Email:    "test2@test",
    }
    result, err := User.FilterWithObject(&user)
    if err != nil {
        fmt.Println("filter user fail")
    }
    for _, each := range result {
        fmt.Println(each)
    }
}

其中FilterWithObject函数的User对象的ID不为空,且大于的时候,执行的SQL如下

1
SELECT * FROM user where id > 0 AND id = 3

ID为空的时候,执行的SQL如下

1
SELECT * FROM user where id > 0 AND username='test2'

ID和Username都为空的时候,执行的SQL如下

1
SELECT * FROM user where id > 0 AND email='test2@test'

参考:GORM Gen使用指南

4.gorm框架关联查询

1.has one

使用外键关联查询,得到一个struct结果,比如查询一个用户,一个用户只有一张信用卡,同时查询用户信息和用户的这张信用卡struct

参考:https://gorm.io/zh_CN/docs/has_one.html

2.has many

使用外键关联查询,得到一个list of struct结果,比如查询一个用户,一个用户拥有多张信用卡,同时查询用户信息和用户所有的信用卡list

参考:https://gorm.io/zh_CN/docs/has_many.html

3.Many to many

Many to Many 会在两个 model 中添加一张连接表。例如,您的应用包含了 user 和 language,且一个 user 可以说多种 language,多个 user 也可以说一种 language。

参考:https://gorm.io/zh_CN/docs/many_to_many.html

 

下面的例子举例说明has one,has many和many2many2如何使用

共有如下几张表

user表

1
2
3
4
5
6
7
CREATE TABLE `user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(128) NOT NULL,
  `email` varchar(128) NOT NULL,
  `department_id` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4

department表

1
2
3
4
5
CREATE TABLE `department` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8mb4 NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

has one:一个user只能属于一个department

card表

1
2
3
4
5
6
CREATE TABLE `card` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `name` varchar(32) CHARACTER SET utf8mb4 NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

has many:一个user可以拥有多张信用卡

role表

1
2
3
4
5
CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) CHARACTER SET utf8mb4 NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

r_user_role

1
2
3
4
5
CREATE TABLE `r_user_role` (
  `user_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  UNIQUE KEY `r_user_role_pk` (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

many2many:user和role之间通过一张r_user_role关联表进行关联,一个user可以拥有多个role,一个role也可以属于多个user

定义UserDetail,其中的Roles为关联属性,如果在表上没有指定外键的话,需要通过 ForeignKey 和 References 标签指定使用哪些字段作为外键关联

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package model
 
type UserDetail struct {
    ID           int64      `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键" json:"id"`                                  // 主键
    Username     string     `gorm:"column:username;comment:用户名" json:"username"`                                                   // 用户名
    Email        string     `gorm:"column:email;comment:邮箱" json:"email"`                                                          // 邮箱
    DepartmentID int32      `gorm:"column:department_id;not null" json:"department_id"`                                            // 部门id
    Department   Department `gorm:"foreignKey:ID;References:DepartmentID"`                                                         // has one
    Cards        []Card     `gorm:"foreignKey:UserID;References:ID"`                                                               // has many
    Roles        []Role     `gorm:"many2many:r_user_role;foreignKey:ID;joinForeignKey:UserID;References:ID;JoinReferences:RoleID"` // many2many2
 
}
 
func (*UserDetail) TableName() string {
    return "user"
}

data查询层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (repo *userRepo) FindUserDetailByID(ctx context.Context, id int64) (*model.UserDetail, error) {
    user := model.UserDetail{}
    err := repo.data.db.WithContext(ctx).
        Model(&model.UserDetail{}).
        Preload("Roles").
        Preload("Department").
        Preload("Cards").
        Where("id = ?", id).Find(&user).Error
    if err != nil {
        repo.log.Error(fmt.Sprintf("find user detail by id fail, error: %v", err))
        return nil, err
    }
    return &user, nil
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
func Test_userRepo_FindUserDetailByID(t *testing.T) {
    data, _, err := NewData(zap_logger, db, rdb)
    if err != nil {
        panic(err)
    }
    userRepo := userRepo{data: data, log: zap_logger}
    result, err := userRepo.FindUserDetailByID(context.Background(), 1)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Print(result)
}

结果如下,可以看到关联的department表,card表和role表中的属性都可以同时查询到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
=== RUN   Test_userRepo_FindUserDetailByID
 
2025/02/22 22:48:20 /Users/lintong/coding/go/gin-template/internal/data/user.go:42
[2.069ms] [rows:2] SELECT * FROM `card` WHERE `card`.`user_id` = 1
 
2025/02/22 22:48:20 /Users/lintong/coding/go/gin-template/internal/data/user.go:42
[2.355ms] [rows:1] SELECT * FROM `department` WHERE `department`.`id` = 1
 
2025/02/22 22:48:20 /Users/lintong/coding/go/gin-template/internal/data/user.go:42
[2.385ms] [rows:2] SELECT * FROM `r_user_role` WHERE `r_user_role`.`user_id` = 1
 
2025/02/22 22:48:20 /Users/lintong/coding/go/gin-template/internal/data/user.go:42
[3.237ms] [rows:2] SELECT * FROM `role` WHERE `role`.`id` IN (1,2)
 
2025/02/22 22:48:20 /Users/lintong/coding/go/gin-template/internal/data/user.go:42
[12.519ms] [rows:1] SELECT * FROM `user` WHERE id = 1
&{1 test test@test 1 {1 开发} [{1 1 信用卡1} {2 1 信用卡2}] [{1 角色1} {2 角色2}]}--- PASS: Test_userRepo_FindUserDetailByID (0.01s)

5.gorm事务

1.data层承载事务

参考:https://gorm.io/zh_CN/docs/transactions.html

2.biz层承载事务

参考:在 Go-Kratos 框架中优雅的使用 GORM 完成事务

以及 https://github.com/go-kratos/examples/blob/main/transaction/gorm/internal/biz/biz.go

 

posted @   tonglin0325  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示