笔记引用:码神之路 https://www.mszlu.com
一、Grom快速入门 官方地址:https://github.com/go-gorm/gorm
官方文档:https://gorm.io/zh_CN/
全功能 ORM 关联 (拥有一个,拥有多个,属于,多对多,多态,单表继承) Create,Save,Update,Delete,Find 中钩子方法 支持 Preload、Joins 的预加载 事务,嵌套事务,Save Point,Rollback To to Saved Point Context、预编译模式、DryRun 模式 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD SQL 构建器,Upsert,锁,Optimizer/Index/Comment Hint,命名参数,子查询 复合主键,索引,约束 自动迁移 自定义 Logger 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus… 每个特性都经过了测试的重重考验 开发者友好 1. 搭建一个最基本的Gin框架 项目结构:
1、main.go
:创建客户端,监听端口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "github.com/gin-gonic/gin" "log" "test.cm/gormtest/router" )func main () { r := gin.Default() router.InitRouter(r) err := r.Run(":8080" ) if err != nil { log.Fatalln(err) } }
2、router.router.go
:
1 2 3 4 5 6 7 8 9 10 package routerimport ( "github.com/gin-gonic/gin" "test.cm/gormtest/api" )func InitRouter (r *gin.Engine) { api.RegisterRouter(r) }
3、api.router.go
:
1 2 3 4 5 6 7 8 package apiimport "github.com/gin-gonic/gin" func RegisterRouter (r *gin.Engine) { r.GET("/save" , SaveUser) }
4、user.go
:真正编写业务逻辑的地方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package apiimport ( "github.com/gin-gonic/gin" "test.cm/gormtest/dao" "time" )func SaveUser (c *gin.Context) { user := &dao.User{ Username: "张三" , Password: "123456" , CreateTime: time.Now().UnixMilli(), } dao.SaveUser(user) c.JSON(200 , user) }
2. 安装gorm、数据库 数据库以目前使用最多的mysql为例。
1 2 3 4 5 6 go get -u gorm.io/driver/mysqlgo get -u gorm.io/gormgo get -u github.com/gin-gonic/gin
涉及到的数据库sql:
1 2 3 4 5 6 7 CREATE TABLE `users` ( `id` int (10 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID' , `username` varchar (30 ) NOT NULL COMMENT '账号' , `password` varchar (100 ) NOT NULL COMMENT '密码' , `createtime` bigint (20 ) NOT NULL DEFAULT 0 COMMENT '创建时间' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4
3. 定义DAO、数据库连接、Service、路由 1、定义与表对应的DAO:dao.user.go
1 2 3 4 5 6 7 8 9 10 11 type User struct { ID int64 Username string `gorm:"column:username"` Password string `gorm:"column:password"` CreateTime int64 `gorm:"column:createtime"` }
2、定义连接数据库:dao.grom.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 package daoimport ( "fmt" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" )var DB *gorm.DBfunc init () { username := "root" password := "123456" host := "127.0.0.1" port := 3306 Dbname := "gorm" dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local" , username, password, host, port, Dbname) db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Info), }) if err != nil { panic ("连接数据库失败, error=" + err.Error()) } DB = db }
3、编写业务逻辑,插入数据:api.user.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package apiimport ( "github.com/gin-gonic/gin" "test.cm/gormtest/dao" "time" )func SaveUser (c *gin.Context) { user := &dao.User{ Username: "张三" , Password: "123456" , CreateTime: time.Now().UnixMilli(), } dao.SaveUser(user) c.JSON(200 , user) }
4、插入数据:dao.user.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 package daoimport "log" type User struct { ID int64 Username string `gorm:"column:username"` Password string `gorm:"column:password"` CreateTime int64 `gorm:"column:createtime"` }func (u User) TableName() string { return "users" }func SaveUser (user *User) { err := DB.Create(user).Error if err != nil { log.Println("insert fail : " , err) } }
5、路由:api.router.go
1 2 3 4 5 6 7 8 9 package apiimport "github.com/gin-gonic/gin" func RegisterRouter (r *gin.Engine) { r.GET("/save" , SaveUser) }
结果:
项目结构:
4. 编写业务的流程 假如我们需要查询用户 GetById
①编写Mapper(dao)
1 2 3 4 5 6 7 8 9 10 11 func GetById (id int ) User { var user User err := DB.Where("id=?" , id).First(&user).Error if err != nil { log.Println("find user error : " , err) } return user }
②编写Service
1 2 3 4 5 6 7 8 func GetUserById (c *gin.Context) { id := c.Param("id" ) idx, _ := strconv.Atoi(id) var user dao.User user = dao.GetById(idx) c.JSON(200 , user) }
③编写路由
1 2 r.GET("/get/:id" , GetUserById)
5. 增删改查的快速入门 🎃①插入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func SaveUser (user *User) { err := DB.Create(user).Error if err != nil { log.Println("insert fail : " , err) } }func SaveUser (c *gin.Context) { user := &dao.User{ Username: "张三" , Password: "123456" , CreateTime: time.Now().UnixMilli(), } dao.SaveUser(user) c.JSON(200 , user) } r.GET("/save" , SaveUser)
🎃②查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func GetById (id int64 ) User { var user User err := DB.Where("id=?" , id).First(&user).Error if err != nil { log.Println("get user by id fail : " , err) } return user }func GetUser (c *gin.Context) { user := dao.GetById(1 ) c.JSON(200 , user) } r.GET("/get" , GetUserById)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func GetAll (c *gin.Context) { user := dao.GetAll() c.JSON(200 , user) }func GetAll () []User { var user []User err := DB.Find(&user).Error if err != nil { log.Println("find user error : " , err) } return user } r.GET("/getAll" , GetAll)
🎃③更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func UpdateById (id int64 ) { err := DB.Model(&User{}).Where("id=?" , id).Update("username" , "lisi" ) if err != nil { log.Println("update users fail : " , err) } }func UpdateUser (c *gin.Context) { dao.UpdateById(1 ) user := dao.GetById(1 ) c.JSON(200 , user) } r.GET("/update" , UpdateUser)
🎃④删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func DeleteUser (id int64 ) { err := DB.Where("id=?" , id).Delete(&User{}) if err != nil { log.Println("delete user fail : " , err) } }func DeleteUser (c *gin.Context) { dao.DeleteUser(1 ) user := dao.GetById(1 ) c.JSON(200 , user) } r.GET("/delete" , DeleteUser)
二、模型定义 前面入门案例中,我们定义了User结构体用来和数据表users做映射,User结构体,我们称之为数据模型,在gorm框架中,操作数据库需要预先定义模型 。
底层都是使用的golang的database标准库,利用反射原理,执行读写操作时,将结构体翻译为sql语句,并将结果转化为对应的模型。
1. 模型定义 假设有一个商品表
1 2 3 4 5 6 7 8 CREATE TABLE `goods` ( `id` int (10 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID,商品Id' , `name` varchar (30 ) NOT NULL COMMENT '商品名' , `price` decimal (10 ,2 ) unsigned NOT NULL COMMENT '商品价格' , `type_id` int (10 ) unsigned NOT NULL COMMENT '商品类型Id' , `createtime` int (10 ) NOT NULL DEFAULT 0 COMMENT '创建时间' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4
将上述表翻译为模型后,如下:
1 2 3 4 5 6 7 type Good struct { Id int Name string Price float64 TypeId int CreateTime int64 `gorm:"column:createtime"` }
默认gorm对struct字段名使用Snake Case 命名风格转换成mysql表字段名(需要转换成小写字母)。
Snake Case命名风格,就是各个单词之间用下划线(_)分隔,例如: CreateTime的Snake Case风格命名为create_time
同时默认情况下,使用ID
做为其主键,使用结构体名称的Snake Case
风格的复数形式做为表名,使用 CreatedAt
、UpdatedAt
字段追踪创建、更新时间。
2. 模型标签 标签定义:
标签定义部分,多个标签定义可以使用分号(;)分隔
gorm常用标签如下:
column 指定列名 gorm:"column:createtime"
primaryKey 指定主键 gorm:"column:id; PRIMARY_KEY"
- 忽略字段 gorm:"-"
可以忽略struct字段,被忽略的字段不参与gorm的读写操作
其他的可以查看官方文档:
https://gorm.io/zh_CN/docs/models.html#embedded_struct
3. 表名映射 复数表名,比如结构体User,默认的表名为users 实现Tabler接口 (TableName
不支持动态变化,它会被缓存下来以便后续使用。) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type Tabler interface { TableName() string }func (User) TableName() string { return "profiles" }func (u User) TableName() string { return "users" }
1 2 3 4 5 6 7 8 9 10 11 func UserTable (user User) func (tx *gorm.DB) *gorm.DB { return func (tx *gorm.DB) *gorm.DB { if user.Admin { return tx.Table("admin_users" ) } return tx.Table("users" ) } } db.Scopes(UserTable(user)).Create(&user)
1 db.Table("deleted_users" )
4. gorm.Model(不建议使用) GORM 定义一个 gorm.Model
结构体,其包括字段 ID
、CreatedAt
、UpdatedAt
、DeletedAt
1 2 3 4 5 6 7 type Model struct { ID uint `gorm:"primaryKey"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` }
GORM 约定使用 CreatedAt
、UpdatedAt
追踪创建/更新时间。如果定义了这种字段,GORM 在创建、更新时会自动填充当前时间。
要使用不同名称的字段,您可以配置 autoCreateTime、autoUpdateTime 标签
如果想要保存 UNIX(毫/纳)秒时间戳,而不是 time,只需简单地将 time.Time 修改为 int 即可。
1 2 3 4 5 6 7 type User struct { CreatedAt time.Time UpdatedAt int Updated int64 `gorm:"autoUpdateTime:nano"` Updated int64 `gorm:"autoUpdateTime:milli"` Created int64 `gorm:"autoCreateTime"` }
可以将它嵌入到您的结构体中,以包含这几个字段,比如
1 2 3 4 5 6 7 8 9 10 11 12 type User struct { gorm.Model Name string }type User struct { ID uint `gorm:"primaryKey"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` Name string }
对于正常的结构体字段,你也可以通过标签 embedded
将其嵌入,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type Author struct { Name string Email string }type Blog struct { ID int Author Author `gorm:"embedded"` Upvotes int32 }type Blog struct { ID int64 Name string Email string Upvotes int32 }
可以使用标签 embeddedPrefix
来为 db 中的字段名添加前缀,例如:
1 2 3 4 5 6 7 8 9 10 11 12 type Blog struct { ID int Author Author `gorm:"embedded;embeddedPrefix:author_"` Upvotes int32 }type Blog struct { ID int64 AuthorName string AuthorEmail string Upvotes int32 }
5. 数据库连接 在入门案例中的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var DB *gorm.DBfunc init () { username := "root" password := "123456" host := "127.0.0.1" port := 3306 Dbname := "gorm" dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local" , username, password, host, port, Dbname) db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Info), }) if err != nil { panic ("连接数据库失败, error=" + err.Error()) } DB = db }
GORM 官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server
连接数据库主要是两个步骤:
配置DSN (Data Source Name) 使用gorm.Open连接数据库 5.1 DSN gorm库使用dsn作为连接数据库的参数,dsn翻译过来就叫数据源名称,用来描述数据库连接信息。一般都包含数据库连接地址,账号,密码之类的信息。
格式:
1 [username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
mysql的dsn的一些例子:
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 username:password@tcp(host:port)/Dbname?charset=utf8&parseTime=True&loc=Local root:123456 @tcp(localhost:3306 )/gorm?charset=utf8&parseTime=True&loc=Local root:123456 @tcp(localhost:3306 )/gorm?charset=utf8&parseTime=True&loc=Local&timeout=10 s root:123456 @tcp(localhost:3306 )/gorm?charset=utf8&parseTime=True&loc=Local&timeout=10 s&readTimeout=30 s&writeTimeout=60 s
要支持完整的 UTF-8 编码,您需要将 charset=utf8
更改为 charset=utf8mb4
5.2 连接数据库 1 2 3 4 5 6 7 8 9 10 import ( "gorm.io/driver/mysql" "gorm.io/gorm" )func main () { dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) }
MySQL 驱动程序提供了 一些高级配置 可以在初始化过程中使用,例如:
1 2 3 4 5 6 7 8 db, err := gorm.Open(mysql.New(mysql.Config{ DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local" , DefaultStringSize: 256 , DisableDatetimePrecision: true , DontSupportRenameIndex: true , DontSupportRenameColumn: true , SkipInitializeWithVersion: false , }), &gorm.Config{})
GORM 允许通过 DriverName
选项自定义 MySQL 驱动,例如:
1 2 3 4 5 6 7 8 9 10 import ( _ "example.com/my_mysql_driver" "gorm.io/driver/mysql" "gorm.io/gorm" ) db, err := gorm.Open(mysql.New(mysql.Config{ DriverName: "my_mysql_driver" , DSN: "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local" , }), &gorm.Config{})
5.3 调试模式 db.Debug() 5.4 连接池配置 1 2 3 4 5 sqlDB, _ := db.DB() sqlDB.SetMaxOpenConns(100 ) sqlDB.SetMaxIdleConns(20 )
三、增删改查 1. 插入数据 Create 1 2 3 4 5 6 7 8 9 10 11 user := User{ Username:"zhangsan" , Password:"123456" , CreateTime:time.Now().Unix(), } db.Create(&user) user.ID result.Error result.RowsAffected
1.1 用指定的字段创建:
1 2 db.Select("username" ,"password" ).Create(&user)
1.2 忽略字段
1 db.Omit("username" ).Create(&user)
1.3 批量插入
1 2 3 4 5 6 7 var users = []User{{Username: "jinzhu1" }, {Username: "jinzhu2" }, {Username: "jinzhu3" }} db.Create(&users)for _, user := range users { user.ID }
使用 CreateInBatches
分批创建时,你可以指定每批的数量,例如:
1 2 3 4 var users = []User{{Username: "jinzhu_1" }, ...., {Username: "jinzhu_10000" }} db.CreateInBatches(users, 100 )
1.4 使用map创建:
1 2 3 4 5 6 7 8 9 db.Model(&User{}).Create(map [string ]interface {}{ "Name" : "jinzhu" , "Age" : 18 , }) db.Model(&User{}).Create([]map [string ]interface {}{ {"Name" : "jinzhu_1" , "Age" : 18 }, {"Name" : "jinzhu_2" , "Age" : 20 }, })
map创建注意,主键不会被填充。
1.5 sql表达式: 先Model指定表,然后用map的形式插入数据
1 2 3 4 5 DB.Model(&User{}).Create(map [string ]interface {}{ "username" : "jinzhu" , "password" : clause.Expr{SQL: "md5(?)" , Vars: []interface {}{"123456" }}, })
1.6 使用原生sql语句:
1 db.Exec("insert into users (username,password,createtime) values (?,?,?)" , user.Username, user.Password, user.CreateTime)
2. 更新数据 Save 创建一个表
1 2 3 4 5 6 7 8 9 CREATE TABLE `goods` ( `id` int (11 ) NOT NULL AUTO_INCREMENT COMMENT '商品id' , `title` varchar (100 ) NOT NULL COMMENT '商品名' , `price` decimal (10 , 2 ) NULL DEFAULT 0.00 COMMENT '商品价格' , `stock` int (11 ) DEFAULT '0' COMMENT '商品库存' , `type` int (11 ) DEFAULT '0' COMMENT '商品类型' , `create_time` datetime NOT NULL COMMENT '商品创建时间' , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4;
goods.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package daoimport "time" type Goods struct { Id int Title string Price float64 Stock int Type int CreateTime time.Time }func (v Goods) TableName() string { return "goods" }func SaveGoods (goods Goods) { DB.Create(&goods) }
goods_test.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package daoimport ( "testing" "time" )func TestSaveGoods (t *testing.T) { goods := Goods{ Title: "毛巾" , Price: 25 , Stock: 100 , Type: 0 , CreateTime: time.Now(), } SaveGoods(goods) }
2.1 保存数据 (方便后面的测试)
1 2 3 4 5 6 goods := Goods{} DB.Where("id = ?" , 1 ).Take(&goods) goods.Price = 100 DB.Save(&goods)
2.2 更新单个列 Update
1 2 3 goods := Goods{} DB.Where("id = ?" , 2 ).Take(&goods) DB.Model(&goods).Update("title" , "hello" )
2.3 更新多列 Updates
1 2 3 4 5 6 7 goods := Goods{} DB.Where("id = ?" , 2 ).Take(&goods) DB.Model(&goods).Updates(Goods{ Title: "hello" , Stock: 200 , })
2.4 更新选定的字段
Select("title")
: 只更新title
字段1 2 3 4 5 6 goods := Goods{} DB.Where("id = ?" , 2 ).Take(&goods) DB.Model(&goods).Select("title" ).Updates(Goods{ Title: "hello" , Stock: 200 , })
排除:
1 2 3 4 5 6 goods := Goods{} DB.Where("id = ?" , 2 ).Take(&goods) DB.Model(&goods).Omit("title" ).Updates(Goods{ Title: "hello" , Stock: 200 , })
也可以组合使用
1 Select("*" ).Omit("title" )
gorm更新必须带条件进行更新,否则会返回错误gorm.ErrMissingWhereClause
,或者启用 AllowGlobalUpdate
模式
1 db.Session(&gorm.Session{AllowGlobalUpdate: true }).Model(&User{}).Update("name" , "jinzhu" )
2.5 表达式
1 2 db.Model(&goods).Update("stock" , gorm.Expr("stock + 1" )) db.Model(&goods).Update(map [string ]interface {}{"stock" : gorm.Expr("stock + 1" )})
2.6 子查询更新
1 2 3 4 5 goods := Goods{} DB.Where("id = ?" , 2 ).Take(&goods) DB.Model(&goods).Update("title" , DB.Model(&User{}).Select("username" ).Where("id=?" , 2 ))
3. 删除数据 Delete 1 2 3 goods := Goods{} DB.Where("id = ?" , 2 ).Take(&goods) DB.Delete(&goods)
1 2 3 DB.Delete(&Goods{}, 1 )
同样的道理,不带条件不能进行删除,必须加一些条件,或者使用原生 SQL,或者启用 AllowGlobalUpdate
模式
1 2 db.Session(&gorm.Session{AllowGlobalUpdate: true }).Delete(&User{})
4. 查询数据(Find/First区别?) 使用First时,需要注意查询不到数据会返回ErrRecordNotFound。
使用Find查询多条数据,查询不到数据不会返回错误。
4.1 查询函数 Take:查询一条记录
First: 根据主键正序排序后,查询第一条数据
Last:根据主键倒序排序后,查询最后一条记录
Find:查询多条记录
Pluck:查询一列值
1 2 var titles []string db.Model(&Goods{}).Pluck("title" , &titles)
当 First、Last、Take 方法找不到记录时,GORM 会返回 ErrRecordNotFound 错误 ,可以通过对比gorm.ErrRecordNotFound
进行判断,或者使用Find和Limit的组合进行查询。
1 2 3 4 5 6 7 8 9 10 11 db.Limit(1 ).Find(&user)func FindGood () { var goods Goods err := DB.Where("id=?" , 2 ).Take(&goods).Error fmt.Println(err) }
4.* where、select、order、分页、count、分组、直接执行sql语句 4.2 where
通过db.Where函数设置条件
函数说明: db.Where(query interface{}, args ...interface{})
参数说明:
query sql语句的where子句, where子句中使用问号(?)代替参数值,则表示通过args参数绑定参数 args where子句绑定的参数,可以绑定多个参数
比如:
1 2 3 db.Where("id in (?)" , []int {1 ,2 ,5 ,6 }).Take(&goods) dB.Where("id=?" , 2 ).Limit(1 ).Find(&goods)
4.3 select
设置select子句, 指定返回的字段
1 2 var goods Goods DB.Select("id" , "title" ).Find(&goods)
也可以写聚合函数
1 2 3 var total int DB.Model(&Goods{}).Select("count(*) as total" ).Pluck("total" , &total) fmt.Println(total)
4.4 order
排序(注意顺序:Order要写在查询前面)
gorm在执行语句的时候,先拼接前面的条件,再执行查询Find 1 2 var goods []Goods DB.Order("id desc" ).Find(&goods)
4.5 分页
通过limit和Offset实现
1 2 3 4 var goods []Goods DB.Order("create_time desc" ).Limit(10 ).Offset(10 ).Find(&goods)
4.6 count
返回查询匹配的行数
1 2 3 var total int64 = 0 DB.Model(Goods{}).Count(&total) fmt.Println(total)
4.7 分组
1 2 3 4 5 6 7 8 9 10 11 12 type Result struct { Type int Total int }var results []Result db.Model(Goods{}).Select("type, count(*) as total" ).Group("type" ).Having("total > 0" ).Scan(&results)
scan类似Find都是用于执行查询语句,然后把查询结果赋值给结构体变量,区别在于scan不会从传递进来的结构体变量提取表名. 这里因为我们重新定义了一个结构体用于保存结果,但是这个结构体并没有绑定goods表,所以这里只能使用scan查询函数。 Group函数必须搭配Select函数一起使用
4.8 直接执行sql语句
1 2 3 4 5 sql := "SELECT type, count(*) as total FROM `goods` where create_time > ? GROUP BY type HAVING (total > 0)" db.Raw(sql, "2022-11-06 00:00:00" ).Scan(&results) fmt.Println(results)
四、事务和Hook 1. 会话 Session 为了避免共用db导致的一些问题,gorm提供了会话模式,通过新建session的形式,将db的操作分离,互不影响。
创建session的时候,有一些配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type Session struct { DryRun bool PrepareStmt bool NewDB bool Initialized bool SkipHooks bool SkipDefaultTransaction bool DisableNestedTransaction bool AllowGlobalUpdate bool FullSaveAssociations bool QueryFields bool Context context.Context Logger logger.Interface NowFunc func () time.Time CreateBatchSize int }
比如说可以禁用默认的事务,从而提供性能,官方说大致能提升30%左右:
1 2 3 4 5 tx := db.Session(&Session{SkipDefaultTransaction: true }) tx.First(&user, 1 ) tx.Find(&users) tx.Model(&user).Update("Age" , 18 )
比如使用PreparedStmt
在执行任何 SQL 时都会创建一个 prepared statement 并将其缓存,以提高后续的效率
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 tx := db.Session(&Session{PrepareStmt: true }) tx.First(&user, 1 ) tx.Find(&users) tx.Model(&user).Update("Age" , 18 ) stmtManger, ok := tx.ConnPool.(*PreparedStmtDB) stmtManger.Close() stmtManger.PreparedSQL stmtManger.Stmts for sql, stmt := range stmtManger.Stmts { sql stmt stmt.Close() }
还有,gorm的db默认是协程安全的,如果使用初始化参数,则db不在协程安全:
1 tx := db.Session(&gorm.Session{Initialized: true })
比如context:
1 2 3 4 5 timeoutCtx, _ := context.WithTimeout(context.Background(), time.Second) tx := db.Session(&Session{Context: timeoutCtx}) tx.First(&user) tx.Model(&user).Update("role" , "admin" )
2. 事务(自动事务、手动事务、嵌套事务、保存点) 2.1 自动事务
1 2 3 4 5 6 7 8 9 10 11 12 db.Transaction(func (tx *gorm.DB) error { if err := tx.Create(&Animal{Name: "Giraffe" }).Error; err != nil { return err } if err := tx.Create(&Animal{Name: "Lion" }).Error; err != nil { return err } return nil })
2.2 嵌套事务
GORM 支持嵌套事务,您可以回滚较大事务内执行的一部分操作,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 db.Transaction(func (tx *gorm.DB) error { tx.Create(&user1) tx.Transaction(func (tx2 *gorm.DB) error { tx2.Create(&user2) return errors.New("rollback user2" ) }) tx.Transaction(func (tx2 *gorm.DB) error { tx2.Create(&user3) return nil }) return nil })
2.3 手动事务
1 2 3 4 5 6 7 8 9 10 11 12 13 tx := db.Begin() tx.Create(...) tx.Rollback() tx.Commit()
比如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 tx := db.Begin() rowsAffected := tx.Model(&goods).Where("stock > 0" ).Update("stock" , gorm.Expr("stock - 1" )).RowsAffectedif rowsAffected == 0 { tx.Rollback() return } err := tx.Create(保存订单).Errorif err != nil { tx.Rollback() } else { tx.Commit() }
2.4 保存点
GORM 提供了 SavePoint
、Rollbackto
方法,来提供保存点以及回滚至保存点功能,例如:
1 2 3 4 5 6 7 8 tx := db.Begin() tx.Create(&user1) tx.SavePoint("sp1" ) tx.Create(&user2) tx.RollbackTo("sp1" ) tx.Commit()
3. Hook(创建时、更新时、删除时、查询时) Hook 是在创建、查询、更新、删除等操作之前、之后调用的函数。
如果您已经为模型定义了指定的方法,它会在创建、更新、查询、删除时自动被调用。如果任何回调返回错误,GORM 将停止后续的操作并回滚事务。
钩子方法的函数签名应该是 func(*gorm.DB) error
3.1 创建
创建时可用的 hook
1 2 3 4 5 6 7 8 9 BeforeSave BeforeCreate AfterCreate AfterSave
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func (u *User) BeforeCreate(tx *gorm.DB) (err error ) { u.UUID = uuid.New() if !u.IsValid() { err = errors.New("can't save invalid data" ) } return }func (u *User) AfterCreate(tx *gorm.DB) (err error ) { if u.ID == 1 { tx.Model(u).Update("role" , "admin" ) } return }
在 GORM 中保存、删除操作会默认运行在事务上, 因此在事务完成之前该事务中所作的更改是不可见的,如果您的钩子返回了任何错误,则修改将被回滚。
1 2 3 4 5 6 func (u *User) AfterCreate(tx *gorm.DB) (err error ) { if !u.IsValid() { return errors.New("rollback invalid user" ) } return nil }
3.2 更新
更新时可用的 hook
1 2 3 4 5 6 7 8 9 BeforeSave BeforeUpdate AfterUpdate AfterSave
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func (u *User) BeforeUpdate(tx *gorm.DB) (err error ) { if u.readonly() { err = errors.New("read only user" ) } return }func (u *User) AfterUpdate(tx *gorm.DB) (err error ) { if u.Confirmed { tx.Model(&Address{}).Where("user_id = ?" , u.ID).Update("verfied" , true ) } return }
3.3 删除
删除时可用的 hook
1 2 3 4 5 BeforeDelete AfterDelete
代码示例:
1 2 3 4 5 6 7 func (u *User) AfterDelete(tx *gorm.DB) (err error ) { if u.Confirmed { tx.Model(&Address{}).Where("user_id = ?" , u.ID).Update("invalid" , false ) } return }
3.4 查询
查询时可用的 hook
代码示例:
1 2 3 4 5 6 func (u *User) AfterFind(tx *gorm.DB) (err error ) { if u.MemberShip == "" { u.MemberShip = "user" } return }
五、高级查询 1. scope 作用域允许你复用通用的逻辑,这种共享逻辑需要定义为类型func(*gorm.DB) *gorm.DB
。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func Paginate (r *http.Request) func (db *gorm.DB) *gorm.DB { return func (db *gorm.DB) *gorm.DB { q := r.URL.Query() page, _ := strconv.Atoi(q.Get("page" )) if page == 0 { page = 1 } pageSize, _ := strconv.Atoi(q.Get("page_size" )) switch { case pageSize > 100 : pageSize = 100 case pageSize <= 0 : pageSize = 10 } offset := (page - 1 ) * pageSize return db.Offset(offset).Limit(pageSize) } } db.Scopes(Paginate(r)).Find(&users) db.Scopes(Paginate(r)).Find(&articles)
2. 智能选择字段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type User struct { ID uint Name string Age int Gender string }type APIUser struct { ID uint Name string } db.Model(&User{}).Limit(10 ).Find(&APIUser{})
3. 子查询 1 2 db.Where("amount > (?)" , db.Table("orders" ).Select("AVG(amount)" )).Find(&orders)
from子查询
1 2 3 4 5 6 db.Table("(?) as u" , db.Model(&User{}).Select("name" , "age" )).Where("age = ?" , 18 ).Find(&User{}) subQuery1 := db.Model(&User{}).Select("name" ) subQuery2 := db.Model(&Pet{}).Select("name" ) db.Table("(?) as u, (?) as p" , subQuery1, subQuery2).Find(&User{})
4. 关联操作 1 2 3 4 5 6 7 CREATE TABLE `gorm`.`user_profiles` ( `id` int (20 ) NOT NULL AUTO_INCREMENT, `sex` tinyint(4 ) NULL DEFAULT NULL , `age` int (10 ) NULL DEFAULT NULL , `user_id` int (20 ) NULL DEFAULT NULL , PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4;
比如有一个用户属性表,查询用户的时候不仅需要查用户名、密码,还要将其性别和年龄都查询出来:
1 2 3 4 5 6 7 8 9 10 11 type UserProfile struct { ID int64 UserId int64 Sex int Age int }func (u UserProfile) TableName() string { return "user_profiles" }
1 2 3 4 5 6 7 type User struct { ID int64 Username string `gorm:"column:username"` Password string `gorm:"column:password"` CreateTime int64 `gorm:"column:createtime"` UserProfile UserProfile }
保存User
1 2 3 4 5 6 7 8 9 10 var user = User{ Username: "ms" , Password: "ms" , CreateTime: time.Now().UnixMilli(), UserProfile: UserProfile{ Sex: 0 , Age: 20 , }, } DB.Save(&user)
会产生两条sql,users表和user_profiles表都有数据
这是因为默认的外键是结构体名字+下划线+id,即UserId或者表字段是user_id
如果将user_profiles表中的user_id改为other_id就会失败。
1 2 3 4 5 6 7 type User struct { ID int64 Username string `gorm:"column:username"` Password string `gorm:"column:password"` CreateTime int64 `gorm:"column:createtime"` UserProfile UserProfile `gorm:"foreignKey:OtherId"` }
只要给UserProfile添加上相应的tag即可。
关联标签
foreignKey 指定当前模型的列作为连接表的外键 references 指定引用表的列名,其将被映射为连接表外键 polymorphic 指定多态类型,比如模型名 polymorphicValue 指定多态值、默认表名 many2many 指定连接表表名 joinForeignKey 指定连接表的外键列名,其将被映射到当前表 joinReferences 指定连接表的外键列名,其将被映射到引用表 constraint 关系约束,例如:OnUpdate
、OnDelete
4.1 查询 1 2 3 4 var users []User err := DB.Preload("UserProfile" ).Find(&users).Error fmt.Println(err) fmt.Println(users)
Preload预加载,直接加载关联关系
也可以使用joins进行加载关联数据:
1 2 3 4 var users []User err := DB.Joins("UserProfile" ).Find(&users).Error fmt.Println(err) fmt.Println(users)
从sql中能看的出来,使用了left join。
如果不想要User的数据,只想要关联表的数据,可以这么做:
1 2 3 4 5 6 var user User DB.Where("id=?" , 25 ).Take(&user)var userProfile UserProfile err := DB.Model(&user).Association("UserProfile" ).Find(&userProfile) fmt.Println(err) fmt.Println(userProfile)