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
本文只发表于博客园和tonglin0325的博客,作者:tonglin0325,转载请注明原文链接:https://www.cnblogs.com/tonglin0325/p/18730884
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?