Gorm查询原理与注意事项

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语句

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值