type User struct {
ID uint
Name string
Email *string
Age uint8
Birthday *time.Time
MemberNumber sql.NullString
ActivatedAt sql.NullTime
CreatedAt time.Time
UpdatedAt time.Time
}
上图是不带tag标签的结构体, 这里我有几个疑问:
Q: string 和 *string 有什么区别?
A: 假设在插入Name时没有填写任何Name, 那么MySQL中会存入""空字符串; 如果Email没有填写任何值, 而MySQL中的值是(NULL), 这就是两者的差距; 因为Golang默认值的原因, string型、int型的默认值不是nil(也不能为nil), 所以需要借助指针来为nil, 与MySQL中的(NULL)映射, 另外 *time.Time 也是如此。
Q: sql.NullString和普通的string有什么区别?
A: MySQL中varchar的值为(NULL), 如果结构体中仅是string, 那么该如何映射是好呢? string又不能映射成nil, 就需要sql.NullString, 其结构体如下, 它对(NULL)做了支持。其他的sql.NullBool…都是相同道理。当然还有一种办法, 当不填写值时, 不以go中的类型默认值进行填充, 可以使用 default:(-) 这个gorm标签。
type NullString struct {
String string
Valid bool // 如果为true, 则代表MySQL字段对应的结构体String字段非空
}
Q: 针对上一个提问, *string 也可以做同样的事情, sql.NullString有什么特殊之处吗?
A: https://stackoverflow.com/questions/40092155/difference-between-string-and-sql-nullstring 查询了资料, 两者确实都能达到相同的效果, 但是针对MySQL来说是没有’nil’这个概念的, 它只有(NULL), 所以标准姿势应该是使用sql.NullString, 因为这里是与MySQL打交道的地方。
标签名 | 说明 |
---|---|
column | 指定 db 列名 |
type | 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not null 、size , autoIncrement … 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT |
size | 指定列大小,例如:size:256 |
primaryKey | 指定列为主键 |
unique | 指定列为唯一 |
default | 指定列的默认值 |
precision | 指定列的精度 |
scale | 指定列大小 |
not null | 指定列为 NOT NULL |
autoIncrement | 指定列为自动增长 |
autoIncrementIncrement | 自动步长,控制连续记录之间的间隔 |
embedded | 嵌套字段 |
embeddedPrefix | 嵌入字段的列名前缀 |
autoCreateTime | 创建时追踪当前时间,对于 int 字段,它会追踪秒级时间戳,您可以使用 nano /milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano |
autoUpdateTime | 创建/更新时追踪当前时间,对于 int 字段,它会追踪秒级时间戳,您可以使用 nano /milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli |
index | 根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情 |
uniqueIndex | 与 index 相同,但创建的是唯一索引 |
check | 创建检查约束,例如 check:age > 13 ,查看 约束 获取详情 |
<- | 设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限 (没权限时写入nil值) |
-> | 设置字段读的权限,->:false 无读权限 |
- | 忽略该字段,- 无读写权限 |
comment | 迁移时为字段添加注释 |
tag的常用标签在MySQL建表时指定好就可以, 大部分都不会用到, 一般为了项目的可读性可以将tag补满。
ID int // 默认的主键
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
上面四个字段是gorm规范的字段, 其会自动对它们进行处理。
user := model.User{Username: "姓名", Password: "666", Age: 18, Sex: 0}
result := db.Create(&user)
user.ID // 返回插入数据的主键
result.Error // 返回 error
result.RowsAffected // 返回插入记录的条数
朴素的插入, 如果结构体没有为一个字段填写值, 那么在插入时会已类型的默认值插入。
具体查看Update类型的第一条 Save 语句。
db.Omit("Password").Create(&user) // 插入时忽略Password字段
db.Create(&userArr) // 插入对象数组即可
db.CreateInBatches(users, 每批数量) // 如果数量太多, 还可以分批插入
BeforeSave, BeforeCreate, AfterSave, AfterCreate // 四个钩子函数, 都会开启事务
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
//return errors.New("AfterCreate,可知是插入后执行,返回error会进行事务回滚")
return nil
}
// 当然, 如果你想针对某次操作session忽略钩子函数, 可以做出一下操作
db.Session(&gorm.Session{SkipHooks: true}).Create(&user)
// 图个方便, 感觉不是很规范
db.Model(&User{}).Create(map[string]interface{}{
{"Name": "jinzhu_1", "Age": 18},
{"Name": "jinzhu_2", "Age": 20},
})
需求: MySQL中sex是int类型仅有0/1,代表男/女, 而Golang中是string类型, 如何将它们做个转换?
// 方法一: 对结构体的字段类型进行处理
// 替换成sex的string类型
type SexString string
// GormDataType MySQL中的数据类型
func (s *SexString) GormDataType() string {
return "int"
}
// Scan 读出时转换
func (s *SexString) Scan(v interface{}) error {
return nil
}
// GormValue 写入时转换
func (s SexString) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
var sexNum int
if s == "男" {
sexNum = 1
} else{
sexNum = 0
}
return clause.Expr{
SQL: "?", // 当然, 这里可以替换复杂的sql语句, 比如一些聚合函数 sum(?), POINT类型转换GeomFromText('POINT(?)') 等等
// Vars的值将替换上方的?
Vars: []interface{}{sexNum},
}
}
// 方法二: 执行时直接处理
db.Model(&user).Create(map[string]interface{}{
"username": "你好",
"sex": clause.Expr{SQL: "?", Vars: []interface{}{
// 性别字符串转int
func(string2 model.SexString) int{
if string2 == "男" {
return 1
}
return 0
}("女"),
}},
})
上方的方法二中, 有用到此表达式。 它可以编译SQL语句, 'SQL’字段 为原SQL语句, ‘Vars’ 字段可以对SQL进行填充。
// 在冲突时,什么都不做
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)
// 在username字段冲突时,将role字段更新为'手动定义的role'
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "username"}},
// 这里是'Assignments', 译为'任务', 表明需自己写表达式
DoUpdates: clause.Assignments(map[string]interface{}{"role": "手动定义的role"}),
}).Create(&user)
// 在username字段冲突时,更新'password'和'age'字段
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "username"}},
// 这里是'Assignments', 译为'赋值列', 表明指定更新列(大小写尽量与MySQL对齐, 实际不对齐也没影响)
DoUpdates: clause.AssignmentColumns([]string{"password", "age"}),
}).Create(&user)
// &user的主键如果存在, 那会作为查询条件
result := db.First(&user) // 按照主键排序且查一条 order by id limit 1
result.RowsAffected // 返回找到的记录数
result.Error // returns error or nil
// 注意: 这一条语句还会再执行一次, 返回所有行数据
rows,_ := result.Rows()
for rows.Next(){
user := &model.User{}
err = db.ScanRows(rows, user)
}
db.Last(&user) // 主键排序, 最后一条
db.Take(&user) // 查一条, 但不排序
对于没有主键的表, First 和 Last 会排序表的第一个字段; 查询到的对象会放入&user中。
同上, 会返回
db.Where("username = ?", "你好").Find(&user)
// Struct
db.Where(&User{Username: "jinzhu", Age: 20}).First(&user)
// Struct 结构体后面又跟了 'age' 参数, 表明只需 age 作为查询条件, username忽略。
db.Where(&User{Username: "jinzhu", Age: 20}, 'age').First(&user)
// Map
db.Where(map[string]interface{}{"id": []int{1,2,3,4,5}}).Find(&userArr) // in (1,2,3,4,5)
注意: Stuct 查询时会忽略类型默认值作为查询条件, 例如: age=0 的查询条件; 你可以使用 map 来查询类型默认值。
db.First(&user, "id = ? and username = ?", 6, "你好")
db.Find(&users, User{Age: 20})
db.Find(&users, map[string]interface{}{"age": 20}) // 还是很多样化的
db.Not("username = ?", "jinzhu").First(&user) // struct 和 map 的形式同上
跟在where后面
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users) // struct 和 map 的形式同上
// 可以使用函数, 例如 sum(age)
db.Select("username", "age").Find(&users)
// 默认是asc, desc需要写进参数中
db.Order("age desc").Order("name").Find(&users)
一般用作分页
// SELECT * FROM users OFFSET 5 LIMIT 10;
db.Limit(10).Offset(5).Find(&users)
加上对应的函数即可(这三个函数顺序不用关心, 转成sql语句时gorm会帮忙调整, 尽量按顺序)。
// 连表查询
db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results)
// joins函数内可以预编译SQL, 下面就
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "我是预编译的邮箱参数").Where("credit_cards.number = ?", "110").Find(&user)
type Result struct {
Name string
Age int
}
var result Result
db.Table("users").Select("name", "age").Where("name = ?", "Antonio").Scan(&result)
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result)
// select `id`,`username`,`age`.... 将字段拼接, 不会仅仅有一个*号
db.Session(&gorm.Session{QueryFields: true}).First(&user)
// 注: 也可以进行全局设置
在MySQL层面会加快一些速度。
// 排他锁, 'for update'
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)
// 针对 `users` 表的共享锁
db.Clauses(clause.Locking{
Strength: "SHARE",
Table: clause.Table{Name: clause.CurrentTable},
}).Find(&users)
// 排他锁, 有锁则不等待 SELECT * FROM `users` FOR UPDATE NOWAIT
db.Clauses(clause.Locking{
Strength: "UPDATE",
Options: "NOWAIT",
}).Find(&users)
// SELECT * FROM users WHERE (name, age, role) IN (("jinzhu", 18, "admin"), ("jinzhu 2", 19, "user"));
db.Where("(name, age, role) IN ?", [][]interface{}{{"jinzhu", 18, "admin"}, {"jinzhu2", 19, "user"}}).Find(&users)
// select * from users
var results []map[string]interface{}
db.Table("users").Find(&results)
// user 如果找不到-> User{Username: "non_existing"}
// 注意: FirstOrInit 的 第二个参数会被放入查询条件
// Where `user`本身的条件 and username = '我会被放入查询条件'
db.FirstOrInit(&user, User{Username: "我会被放入查询条件"})
// 不会作为查询条件 可以使用 Attrs() 函数, 查询不到时作为默认字段
db.Attrs(model.User{Age: 20}).FirstOrInit(&user)
// 不会作为查询条件 但不管有没有查到都会替换默认字段 Assign()函数
db.Assign(model.User{Age: 20}).FirstOrInit(&user)
// 默认的第二个参数, 会加入查询条件, 会作为插入值
大致同上, 略
// Attrs()函数 不会作为查询条件, 只会作为插入值
大致同上, 略
// Assign()函数 不会作为查询条件, 没查询到->进行插入操作 查询道理->进行更新操作(必定进行,即使库中数据相同)
大致同上, 略
// 三个函数
IgnoreIndex()// 忽略某某索引
UseIndex() // 建议使用某某索引, MySQL考虑采纳
ForceIndex() // 必须使用某某索引, MySQL必须采纳
// SELECT * FROM `users` USE INDEX (`idx_user_name`)
db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})
rows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Rows()
// 记得关闭rows()
defer rows.Close()
for rows.Next() {
var user User
// ScanRows 方法用于将一行记录扫描至结构体
db.ScanRows(rows, &user)
}
// 每次查询100条, 并且结果会传入匿名函数
result := db.FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
for _, result := range results {
// 批量处理找到的记录
}
// 如果返回错误会终止后续批量操作
return nil
})
// 仅有一个 AfterFind
func (u *User) AfterFind(tx *gorm.DB) (err error) {
if u.Age > 100 {
u.Age = 18
}
return nil
}
// 成年人封装
func GrownUpAge(db *gorm.DB) *gorm.DB {
return db.Where("age >= ?", 18)
}
// 放入封装函数
db.Scopes(GrownUpAge).Find(&user)
// 分页
func PageHelper(pageNum int, pageSize int) func(db *gorm.DB) *gorm.DB{
return func(db *gorm.DB) *gorm.DB {
offset := (pageNum - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
db.Scopes(PageHelper(1, 10)).Find(&user)
var count int64
// SELECT count(1)
db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count)
// SELECT COUNT(DISTINCT(`name`)) FROM `users`
db.Model(&User{}).Distinct("name").Count(&count)
// 会更新所有字段, 即使字段是零值, 比如createdAt也会进行更新, 可配合Omit()去忽略
db.Save(&user)
// 1. 进行更新, 如果返回的更新行数, 如果行数为0, 则进入第二条。
// 2. 行数为0, 执行Take仅根据id查询, 判断现在是否有数据了, 有 -> 进入第三条; 没有 -> 进入第四条
// 3. 有数据, 那么会重新进入1去进行修改。
// 4. 没有数据, 那么会执行Create创建。
// 切记, 如果使用了Model,且&user{}指定了主键, 那么主键也会被加入Where条件
// update users set name = 'hello' where active = true and id = 5;
db.Model(&User{id:6,age:18}).Where("active = ?", true).Update("name", "hello")
// 根据结构体更新, 只会更新结构体的非零字段, 借助Model()参数的主键
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// Map更新
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// Omit 忽略更新某些字段
db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// 结构体配select会更新 select('xx') 里面的参数
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})
// Select * 会更新所有字段(包括零值)
db.Model(&user).Select("*").Update(User{Name: "jinzhu", Role: "admin", Age: 0})
BeforeSave、BeforeUpdate、AfterSave、AfterUpdate
同insert语句, 四种拦截器
// 执行下面语句, 会抛出gorm.ErrMissingWhereClause错误, 因为是全局更新, 无where条件
db.Model(&User{}).Update("name", "jinzhu")
// 可以使用原生sql 或者 开启全局更新
db.Session(&gorm.Session{AllowGlobalUpdate: true})
db.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))
// 拦截器和时间统计 不生效
db.Model(&user).UpdateColumn("name", "hello")
db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18})
Changed方法只能与 Update、Updates 方法一起使用,并且它只是检查 Model 对象字段的值与 Update、Updates 的值是否相等(不是查MySQL中的数据比较)。
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
// 如果 Role 字段有变更, 返回错误,停止修改
if tx.Statement.Changed("Role") {
return errors.New("role not allowed to change")
}
// // 如果 Name 或 Role 字段有变更, 就修改 age 为 18
if tx.Statement.Changed("Name", "Admin") {
tx.Statement.SetColumn("Age", 18)
}
// 任意字段是否有变更
if tx.Statement.Changed() {
}
return nil
}
// 根据 userId 删除
db.Delete(&user)
// DELETE from emails where id = 10 AND name = "jinzhu";
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE FROM users WHERE id IN (1,2,3);
db.Delete(&users, []int{1,2,3})
`BeforeDelete`、`AfterDelete`
// BeforeDelete(tx *gorm.DB) (err error)
// AfterDelete(tx *gorm.DB) (err error)
同前系列的钩子方法; 删除前, 删除后;
// 批量删除
db.Where("email LIKE ?", "%jinzhu%").Delete(Email{})
// 自动阻止无where条件的删除语句
db.Delete(&User{})
// 允许无Where删除
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{})
DB.Clauses(clause.Returning{}).Where("role = ?", "admin").Delete(&users)
// 还可以设置返回结构体中指定字段
clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "age"}}}
模型包含了一个 gorm.deletedat
字段即可, 推荐字段名称 DeletedAt。
// 使用 Unscoped() 函数
db.Unscoped().Where("age = 20").Find(&users)
// 使用 Unscoped() 函数
db.Unscoped().Delete(&order)
可知, 逻辑删除是靠 Scoped 封装函数实现的。
// 结构体字段 `soft_delete.DeletedAt`类型即可 [gorm.io/plugin/soft_delete]
DeletedAt soft_delete.DeletedAt
Q: 软删除字段要加唯一索引怎么办?
A: 和另一个字段生成复合索引。
如果不想用时间记录逻辑删除, 只想要个 bool 记录, 那么可以如下:
// softDelete 标签加上即可, 0是未删除 1是删除
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
// 查询未删除的所有记录
SELECT * FROM users WHERE is_del = 0;
// 单单 Raw 不能单单进行执行, 需要 Scan 进行执行
db.Raw("SELECT id, name, age FROM users WHERE name = ?", 3).Scan(&result)
// Exec 执行
db.Exec("UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3})
// 并非原 sql, 参数会被 '?' 占领
stmt := db.Session(&gorm.Session{DryRun: true}).First(&user, 1).Statement
stmt.SQL.String() //=> SELECT * FROM `users` WHERE `id` = ? ORDER BY `id`
stmt.Vars //=> []interface{}{1}
// 原 SQL, 占位符已经被填入了
sqlStr := DB.ToSQL(func(tx *gorm.DB) *gorm.DB {
return tx.Model(&User{}).Where("id = ?", 100).Limit(10).Order("age desc").Find(&[]User{})
})
// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{SkipDefaultTransaction: true,})
// 单次禁用
tx := db.Session(&gorm.Session{SkipDefaultTransaction: true})
db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
// 返回 nil 提交事务
return nil
})
外层事务 | 小事务1号 | 小事务2号 | 结果 |
---|---|---|---|
成功 | 失败 | 失败 | 两个小事务回滚, 外层事务提交 |
失败 | 成功 | 成功 | 都回滚 |
成功 | 失败 | 成功 | 仅小事务1号回滚 |
事务会进行传播, 相同级别不影响, 外层事务失败了会影响内层事务。
// 外层事务
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") // Rollback user2
})
// 小事务二号
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&user3)
return nil
})
return nil
})
// 开始事务
tx := db.Begin()
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)
// 遇到错误时回滚事务
tx.Rollback()
// 否则,提交事务 tx.Commit().Error 可以查看错误
tx.Commit()
MySQL自带的中途存档和归档功能
1、使用 SAVEPOINT identifier 来创建一个名为identifier的回滚点
2、ROLLBACK TO identifier,回滚到指定名称的SAVEPOINT,这里是identifier
3、 使用 RELEASE SAVEPOINT identifier 来释放删除保存点identifier
// 运用MySQL功能, 貌似没有Release释放存档的API
tx := db.Begin()
tx.Create(&user1)
tx.SavePoint("sp1")
tx.Create(&user2)
tx.RollbackTo("sp1") // Rollback user2
tx.Commit() // Commit user1
gorm提供了DDL层面的语句, 如 删除或创建表、修改字段、创建修改索引这些API。
具体略。
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Silent, // 日志级别
// 忽略ErrRecordNotFound(记录未找到)错误, 一些API例如 First()
IgnoreRecordNotFoundError: true,
Colorful: false, // 禁用彩色打印
},
)
对于一些频繁SQL可以使用预编译提高速率
// 全局
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
PrepareStmt: true,
})
// 单词
tx := db.Session(&gorm.Session{PrepareStmt: true})
TableName()
方法, 返回表名。gorm:"primaryKey"
标签进行重新指认主键。gorm:"column:hello"
标签自定义MySQL中对应的字段名称。 1. 全局式 Set(k, v)
& Get(k)
// 使用Set()函数
db.Set("my_value", myValue).Create(&User{})
// 在钩子方法或在其他地方可以使用Get()函数获取
myValue, ok := tx.Get("my_value")
2. 一次Session式 InstanceSet(k, v)
& InstanceGet(k)
同上, 仅函数作了改变;
一般不要用户输入的数据直接拼接到 SQL 中, 需要通过 ?
字符, 进行预编译生成。
// 不推荐
db.Where("name = " + name).First(&user)
// 推荐
db.Where("name = ?", name).First(&user)