Gorm使用手册
连接数据库
对于数据库的操作肯定第一步就是连接它了
// 连接数据库的字符串信息,里面包括地址,用户名密码,时间。
dsn := "root:123123@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
// 第一个参数指定了原生数据库连接类,这里使用mysql原生操作,我们可以指定驱动名称,也可以将创建好的连接直接传入。第二个是gorm的配置包括连接池、日志系统等。
open, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
让我们先进行一个简单的查询操作
// 结果对象
exam := &model.ElExam{}
// 设置要查询的表,使用结果体,根据ID排序查询第一条数据
common.Db.Debug().Table("el_exam").Model(model.ElExam{}).First(exam)
//这是打印出来的sql语句
[7.046ms] [rows:1] SELECT * FROM `el_exam` ORDER BY `el_exam`.`id` LIMIT 1
这里使用了Debug() 它的作用是将使用的SQL语句打印出来。
Model方法的作用是标识结果集的类型。
在传统业务中,查询操作占了大多数,所以我们开始对查询操作进行探究。
查询
使用
gorm的sql对应的方法非常简单,就是相对应的操作对应相对应的名称,比如条件就使用where() 、分组Group() 、排序Order(),至于里面的参数统一采用了第一个参数为语句,从第二个参数开始为传入的值,比如where()
//第一个参数为查询的语句,第二个参数就是对应?的地方。
Where( "name = ?" , "jinzhu" )
//第二个参数为动态参数,所以如果不传入值时会将第一个参数直接进行拼接操作,比如
Where( "name = ?")
// 拼接后是 where name=? 这个问号会当做语句的一部分。
// 我们除了直接传参数以外还可以使用结构体,map的形式传参
// 结构体传参,这里需要注意结构体中属性的初始值会被忽略掉,比如sex = false会被忽略掉,age = 0 会被忽略掉
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;
// map啧没有这个问题
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
// 数组会被解析成in的形式
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);
对于简单的语句调用请访问https://gorm.io/zh_CN/docs/query.html
接下来讲解几个常用的方法
// 根据tx生成sql语句
// db.ToSQL(func(tx *gorm.DB) *gorm.DB {
// return tx.Model(&User{}).Where(&User{Name: "foo", Age: 20})
// .Limit(10).Offset(5)
// .Order("name ASC")
// .First(&User{})
// })
func (db *DB) ToSQL(queryFn func(tx *DB) *DB)
// 创建一个新的会话窗口,我们在调用model,table方法时都会拷贝当前会话,这样会造成上一个会话窗口中的某些数据被携带进来,比如查询时的过滤条件
tx := common.Db.Table("student")
tx.Where("name","zhangsan").First(&user{})
tx.Where("name", "lisi").First(&user{})
//生成的代码为select * FROM student where name =zhangsan and name = lisi
// 如果我们不想让session拷贝怎么办那,我们在session中配置newDb为true即可
db.Session(&gorm.Session{
NewDB: true,
})
// 我们可以手动编写sql语句然后调用Raw将SQL语句添加到db中。调用Scan进行语句运行。以及结果集映射
db.Raw("SELECT id, name, age FROM users WHERE name = ?", 3).Scan(&result)
// 运行原生sql语句
db.Exec("UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3})
// 我们可以使用sql.Named或map[string]interface{}{}来指定填充的对象
db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu"}).First(&user)
// SELECT * FROM `users` WHERE name1 = "jinzhu" OR name2 = "jinzhu" ORDER BY `users`.`id` LIMIT 1
SQL生成原理
对于sql的生成,比较简单,我们调用的Where()还是Select()都实现type Expression interface接口,该接口就是用来模块拼接的。
// 实现该接口的结构体有: Eq、Expr、From、Create、Update、Hints、IN等,基本包括了所有sql语句关键字。
//接下来让我们对Where进行详细讲解。
type Expression interface {
Build(builder Builder)
}
type Where struct {
Exprs []Expression
}
// 这里返回拼接字符串中的Where字符
func (where Where) Name() string {
return "WHERE"
}
// 这里会构建Where后面的条件判断
func (where Where) Build(builder Builder) {
buildExprs(where.Exprs, builder, AndWithSpace)
}
func buildExprs(exprs []Expression, builder Builder, joinCond string) {
wrapInParentheses := false
for idx, expr := range exprs {
expr.Build(builder)
}
}
// 一般Exprs里面都是clause.Expr类型
// 例如我们调用Where("1=?",1)
type Expr struct {
// 这里就是存储的第一个参数 "1=?"。
SQL string
// 从第二个参数开始进行存储。
Vars []interface{}
// 是否有括号
WithoutParentheses bool
}
func (expr Expr) Build(builder Builder) {
var (
afterParenthesis bool
idx int
)
for _, v := range []byte(expr.SQL) {
// 判断是否为 ? 如果是进行渲染
if v == '?' && len(expr.Vars) > idx {
if afterParenthesis || expr.WithoutParentheses {
if _, ok := expr.Vars[idx].(driver.Valuer); ok {
builder.AddVar(builder, expr.Vars[idx])
} else {
switch rv := reflect.ValueOf(expr.Vars[idx]); rv.Kind() {
case reflect.Slice, reflect.Array:
if rv.Len() == 0 {
builder.AddVar(builder, nil)
} else {
for i := 0; i < rv.Len(); i++ {
if i > 0 {
builder.WriteByte(',')
}
builder.AddVar(builder, rv.Index(i).Interface())
}
}
default:
builder.AddVar(builder, expr.Vars[idx])
}
}
} else {
builder.AddVar(builder, expr.Vars[idx])
}
idx++
} else {
if v == '(' {
afterParenthesis = true
} else {
afterParenthesis = false
}
builder.WriteByte(v)
}
}
if idx < len(expr.Vars) {
for _, v := range expr.Vars[idx:] {
builder.AddVar(builder, sql.NamedArg{Value: v})
}
}
}
上面我们看到了Where的生成原理,那么有同学要问,Where参数也可能是Map与结构体的形式那应该怎么处理那,其实Where()在调用时会调用tx.Statement.BuildCondition(query, args…)对结构体或map解析成上Exprs
既然我们已经明白了sql生成原理,那么不掉用where等操作,手动添加一些关键字那,比如说查询时添加索引。
import "gorm.io/hints"
// 我们可以看到,参数类型就是clause.Expression。
// 上面关键字添加操作都是使用了db.Clauses完成的,比如说Where,就是调用了AddClause()方法
func (db *DB) Clauses(conds ...clause.Expression) (tx *DB)
// Clauses Add clauses
func (db *DB) Clauses(conds ...clause.Expression) (tx *DB) {
tx = db.getInstance()
var whereConds []interface{}
for _, cond := range conds {
// 这里判断是否是关键字,比如Select、from、where、delete等
if c, ok := cond.(clause.Interface); ok {
tx.Statement.AddClause(c)
} else if optimizer, ok := cond.(StatementModifier); ok {
// 对于索引操作,会在代码生成最后进行处理
optimizer.ModifyStatement(tx.Statement)
} else {
// where条件操作 比如使用 Eq,Gt等操作
whereConds = append(whereConds, cond)
}
}
// 这里我们可以看到,会将Where条件解析成clause.Where格式添加到关键字里面
if len(whereConds) > 0 {
tx.Statement.AddClause(clause.Where{Exprs: tx.Statement.BuildCondition(whereConds[0], whereConds[1:]...)})
}
return
}
// 例子
// 添加关键字 MAX_EXECUTION_TIME(10000)
db.Clauses(hints.New("MAX_EXECUTION_TIME(10000)")).Find(&User{})
// SELECT * /*+ MAX_EXECUTION_TIME(10000) */ FROM `users`
import "gorm.io/hints"
// 添加关键字 USE INDEX
db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})
// SELECT * FROM `users` USE INDEX (`idx_user_name`)
db.Clauses(hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin()).Find(&User{})
// SELECT * FROM `users` FORCE INDEX FOR JOIN (`idx_user_name`,`idx_user_id`)"
Gorm配置
接下来我们讲解一下在初始化DB时的配置类
//在open方法的两个参数都是配置类,我们一一讲解
open, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
//第一个根据使用的数据库确定配置类,这里使用的mysql所以我们查看mysql相关的配置类
type Config struct {
// 驱动名称 默认mysql
DriverName string
// 服务器版本 在初始化时确定
ServerVersion string
// 连接字符串,包含服务器地址,用户名密码
DSN string
// 连接池
Conn gorm.ConnPool
// 根据当前 MySQL 版本自动配置
SkipInitializeWithVersion bool
// string 类型字段的默认长度
DefaultStringSize uint
// 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
DisableDatetimePrecision bool
// 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
DontSupportRenameIndex bool
// 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
DontSupportRenameColumn bool
DontSupportForShareClause bool
DontSupportNullAsDefaultValue bool
}
//第二个参数为gorm配置信息
type Config struct {
// 默认开启事务,是否禁止
SkipDefaultTransaction bool
// 表,列命名策略 需要实现Namer 默认实现schema.NamingStrategy
NamingStrategy schema.Namer
// FullSaveAssociations full save associations
FullSaveAssociations bool
// 日志配置,调用logger.New创建
Logger logger.Interface
// 更改创建时间使用的函数
NowFunc func() time.Time
// 生成 SQL 但不执行
DryRun bool
// 预加载功能
PrepareStmt bool
// 禁用自动 Ping 自动检测数据库的可用性
DisableAutomaticPing bool
// 在 AutoMigrate 或 CreateTable 时,GORM 会自动创建外键约束,若要禁用该特性,可将其设置为 true
DisableForeignKeyConstraintWhenMigrating bool
// 禁止嵌套事务
DisableNestedTransaction bool
// 全局更新
AllowGlobalUpdate bool
// QueryFields 使用表的所有字段执行 SQL 查询 不开启不指定select则使用*
QueryFields bool
// CreateBatchSize 默认创建批量大小
CreateBatchSize int
// ConnPool 数据库连接池
ConnPool ConnPool
// 插件
Plugins map[string]Plugin
//DB.set() DB.get()时的存储类
cacheStore *sync.Map
}
该配置适用于全局配置,在我们使用过程中,可以通过Session()来更改gorm.config,但是仅限于Session返回后的DB。
钩子
我们可以通过SkipHooks = true配置跳过钩子
对于钩子我们需要在Model类中实现以下钩子函数,
比如 db.Model(User{}).find(&user)
那么我们需要 (更新,删除,查询一致)
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
}
创建钩子
BeforeSave
BeforeCreate
// 关联前的 save
// 插入记录至 db
// 关联后的 save
AfterCreate
AfterSave
更新钩子
// 开始事务
BeforeSave
BeforeUpdate
// 关联前的 save
// 更新 db
// 关联后的 save
AfterUpdate
AfterSave
// 提交或回滚事务
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
}
删除钩子
// 开始事务
BeforeDelete
// 删除 db 中的数据
AfterDelete
// 提交或回滚事务
// 在同一个事务中更新数据
func (u *User) AfterDelete(tx *gorm.DB) (err error) {
if u.Confirmed {
tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("invalid", false)
}
return
}
查询
// 从 db 中加载数据
// Preloading (eager loading)
AfterFind
func (u *User) AfterFind(tx *gorm.DB) (err error) {
if u.MemberShip == "" {
u.MemberShip = "user"
}
return
}
GORM 通过 WithContext
方法提供了 Context 支持
可以在钩子中使用它
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
ctx := tx.Statement.Context
// ...
return
}
错误处理
对于错误处理需要通过返回的DB.Error进行获取
if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil {
// 处理错误...
}
if result := db.Where("name = ?", "jinzhu").First(&user); result.Error != nil {
// 处理错误...
errors.Is(err, gorm.ErrRecordNotFound)
}
错误代码请参考https://github.com/go-gorm/gorm/blob/master/errors.go
事务
有两种使用方法
db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
return err
}
// 返回 nil 提交事务
return nil
})
// 手动事务
// 开始事务
tx := db.Begin()
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)
// 遇到错误时回滚事务
tx.Rollback()
// 否则,提交事务
tx.Commit()
嵌套事务
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
})
SavePoint、RollbackTo
tx := db.Begin()
tx.Create(&user1)
tx.SavePoint("sp1")
tx.Create(&user2)
tx.RollbackTo("sp1") // Rollback user2
tx.Commit() // Commit user1
这个原理很简单,就是调用了Exec运行SavePoint SQL语句