gorm 浅入浅出

Written by with ♥ on in Go

Model and Tag

gorm 提供多种 Tag 对应数据库中的多种类型。

  • primary_key 主键:结构体的 IDId 字段默认为主键字段,可以不添加 gorm:"primary_key" 标签。
  • auto_increment 字段是否自增,primary_key 会自带 auto_increment 可以使用 auto_increment:false 屏蔽
  • unique 是否唯一
  • unique_index 类似 index 创建唯一索引
  • not null 是否可以为空
  • default:1 数据插入默认值
  • type:varchar(255) 数据类型
  • index:indexName 索引
  • column:name 结构体字段对应的数据库字段
  • gorm:"-" 忽略该字段
  • size 字段长度
  • PRELOAD:false 禁止预加载

Example:

  • intgorm:"type:tinyint;not null;default:0"
  • displaygorm:"type:tinyint;not null;default:1"
  • stringgorm:"type:varchar(100);not null"gorm:"type:text"
type User struct {
    gorm.Model
    Name         string
    Age          sql.NullInt64
    Birthday     *time.Time
    Email        string  `gorm:"type:varchar(100);unique_index"`
    Role         string  `gorm:"size:255"` // set field size to 255
    MemberNumber *string `gorm:"unique;not null"` // set member number to unique and not null
    Num          int     `gorm:"auto_increment"` // set num to auto incrementable
    Address      string  `gorm:"index:addr"` // create index with name `addr` for address
    IgnoreMe     int     `gorm:"-"` // ignore this field
}

gorm.Model 的定义如下:

type Model struct {
	ID        uint `gorm:"primary_key"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt *time.Time `sql:"index"`
}

设置默认表名前缀

gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string {   
    return "prefix_" + defaultTableName   }

Migrate

使用 AutoMigrate 函数创建表:

建表测试:

type User struct {
	gorm.Model
	Username string `gorm:"type:varchar(255);not null"`
	Mobile string `gorm:"type:varchar(11);not null;index:mobile;unique"`
	Age int `gorm:"default:1"`
	Active bool
}

DB.AutoMigrate(&User{})

结果如下:

CREATE TABLE `users` (`id` int unsigned AUTO_INCREMENT,`created_at` timestamp NULL,`updated_at` timestamp NULL,`deleted_at` timestamp NULL,`username` varchar(255) NOT NULL,`mobile` varchar NOT NULL UNIQUE,`age` int DEFAULT 1,`active` boolean , PRIMARY KEY (`id`))

CREATE INDEX mobile ON `users`(`mobile`)

CREATE INDEX idx_users_deleted_at ON `users`(deleted_at)

bool 类型在 Mysql 底层会转为 tindyint 并且 false = 0true = 1

表名规则

默认的表名规则是结构体的复数形式,可以通过设置禁用。

db.SingularTable(true) // 如果设置为true,`User`的默认表名为`user`,使用`TableName`设置的表名不受影响

// 设置默认前缀
gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string  {
    return "prefix_" + defaultTableName;
}

// TableName 可以更细化得指定表名
func (u User) TableName() string {
    if u.Role == "admin" {
        return "admin_users"
    } else {
        return "users"
    }
}

Set

func (s *DB) Set(name string, value interface{}) *DB

Set set setting by name, which could be used in callbacks, will clone a new db, and update its setting

// 设置引擎 字符集
// 注意 DB.Set 返回一个新的 DB 实例,建表用新实例
DB = DB.Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8")

DEFAULT 约束用于向表中插入默认值,插入数据时没指定值则使用默认值。

CRUD

下面分别解析 CRUD。

var user User
user.Id = 1
db.Model(&user)

Model 函数中的 user 是源,必须包含主键。

创建

db.Create()

批量创建

下面是个插入 Order 结构体中 28 个字段的例子:

  • fieldNums 设置插入的字段数(对应占位符 ? 的数量);
  • for range 中的字段添加的顺序与下面 Insert 语句顺序要对上。
func BulkInsertOrders(db *gorm.DB, orders []*Order) error {
	if len(orders) == 0 {
		return nil
	}

	fieldNums := 28
	quesMarkString := "("
	for i := 0; i < fieldNums; i++ {
		quesMarkString += "?, "
	}
	quesMarkString = quesMarkString[:len(quesMarkString)-2] + ")"

	valueStrings := make([]string, 0, len(orders))
	valueArgs := make([]interface{}, 0, len(orders)*fieldNums)

	for _, ord := range orders {
		valueStrings = append(valueStrings, quesMarkString)
		valueArgs = append(valueArgs, ord.OrderSn)
		valueArgs = append(valueArgs, ord.GoodsId)
		valueArgs = append(valueArgs, ord.GoodsName)
		valueArgs = append(valueArgs, ord.GoodsThumbnailUrl)
		valueArgs = append(valueArgs, ord.GoodsQuantity)
		valueArgs = append(valueArgs, ord.GoodsPrice)
		valueArgs = append(valueArgs, ord.OrderAmount)
		valueArgs = append(valueArgs, ord.OrderCreateTime)
		valueArgs = append(valueArgs, ord.OrderSettleTime)
		valueArgs = append(valueArgs, ord.OrderVerifyTime)
		// 10
		valueArgs = append(valueArgs, ord.OrderReceiveTime)
		valueArgs = append(valueArgs, ord.OrderPayTime)
		valueArgs = append(valueArgs, ord.PromotionRate)
		valueArgs = append(valueArgs, ord.PromotionAmount)
		valueArgs = append(valueArgs, ord.BatchNo)
		valueArgs = append(valueArgs, ord.OrderStatus)
		valueArgs = append(valueArgs, ord.OrderStatusDesc)
		valueArgs = append(valueArgs, ord.VerifyTime)
		valueArgs = append(valueArgs, ord.OrderGroupSuccessTime)
		valueArgs = append(valueArgs, ord.OrderModifyAt)
		// 20
		valueArgs = append(valueArgs, ord.Type)
		valueArgs = append(valueArgs, ord.GroupId)
		valueArgs = append(valueArgs, ord.AuthDuoId)
		valueArgs = append(valueArgs, ord.ZsDuoId)
		valueArgs = append(valueArgs, ord.CustomParameters)
		valueArgs = append(valueArgs, ord.Pid)
		valueArgs = append(valueArgs, ord.MatchChannel)
		valueArgs = append(valueArgs, ord.DuoCouponAmount)
	}
	stmt := fmt.Sprintf("INSERT INTO pdd_order_persistence (order_sn, goods_id, goods_name, goods_thumbnail_url, goods_quantity, goods_price, "+
		"order_amount, order_create_time, order_settle_time, order_verify_time, order_receive_time, order_pay_time, promotion_rate, promotion_amount, batch_no, "+
		"order_status, order_status_desc, verify_time, order_group_success_time, order_modify_at, type, group_id, auth_duo_id, zs_duo_id, custom_parameters, pid, match_channel, duo_coupon_amount"+
		") VALUES %s", strings.Join(valueStrings, ","))
	err := db.Exec(stmt, valueArgs...).Error
	return err
}

查询

Find 会使用结构体对应的表名(通过函数定义或结构体的复数形式)查询数据。

First 本质就是限制 Limit 为 1 的 Find

Scan 不会带入表名,需要使用 DB.Table() 指定。

// Get all matched recordsdb.Where("name = ?", "jinzhu")
.Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu'; db.Where("name <> ?", "jinzhu").Find(&users)  
// INdb.Where("name in (?)", []string{"jinzhu", "jinzhu 2"}).Find(&users) 
// LIKEdb.Where("name LIKE ?", "%jin%").Find(&users)  
// ANDdb.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users) 
// Timedb.Where

db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)

预加载 Preload

预加载某些字段的值。

package main

import (
    "github.com/davecgh/go-spew/spew"
    _ "github.com/lib/pq"

    "github.com/jinzhu/gorm"
)

type Country struct {
    ID int

    Translations []CountryTranslation
}

type CountryTranslation struct {
    CountryID int
    Locale    string
    Name      string
}

type Region struct {
    ID        int
    CountryID int

    Translations []RegionTranslation
    Country      Country
}

type RegionTranslation struct {
    RegionID int
    Locale   string
    Name     string
}

func main() {
    db, err := gorm.Open("postgres", "user=gorm DB.name=gorm sslmode=disable")
    if err != nil {
        panic(err)
    }

    db.LogMode(true)
    db.AutoMigrate(&Country{}, &CountryTranslation{}, &Region{}, &RegionTranslation{})

    var regions []Region
    db.Save(&Region{
        Country: Country{
            Translations: []CountryTranslation{
                {Locale: "zh", Name: "ZH"},
                {Locale: "en", Name: "US"},
            },
        },
        Translations: []RegionTranslation{
            {Locale: "zh", Name: "ZH"},
            {Locale: "de", Name: "DE"},
        },
    })

    db.Save(&Region{
        Country: Country{
            Translations: []CountryTranslation{
                {Locale: "zh", Name: "ZH"},
                {Locale: "en", Name: "US"},
            },
        },
        Translations: []RegionTranslation{
            {Locale: "zh", Name: "ZH"},
            {Locale: "de", Name: "DE"},
        },
    })
    db.Preload("Translations", "locale = ?", "zh").
        Preload("Country").
        Preload("Country.Translations", "locale = ?", "zh").
        Find(&regions)
    spew.Dump(regions)
}

更新

Save() 更新所有字段

Save 会更新所有字段,并且会更新或创建关联模型的数据。

更新修改字段

Update() 更新单个字段

Updates() 传入 struct 只会更新非零值的字段

上面的会带上 updated_at 字段更新。

无 Hooks 更新

上面的方法会更自动运行 model 的 BeforeUpdateAfterUpdate 方法,更新 UpdatedAt 时间戳和在更新时保存关联,不需要这些可以调用

UpdateColumn()UpdateColumns()

// Model 会使用结构体 x 的主键去查询数据
// 如果主键为 0 值,会更新表中所有数据(注意防范)
// UPDATE `question_collect` SET `display` = '0'
DB.Model(x).Update("display", 0)

删除

unscope 硬删除!

关联关系 associations

关系型数据库:关联关系相关接口。

Belongs To 属于

belongs to 是一对一关系,一个模型属于另一个模型。

type User struct {
  gorm.Model
  Name string
}

// `Profile` 属于 `User`, 外键是`UserID` 
type Profile struct {
  gorm.Model
  UserID int
  User   User
  Name   string
}

外键 foreignkey

默认外键使用所有者的类型名称加上其主键,User 的外键就是 UserId。

type User struct {
  gorm.Model
  Name string
}

type Profile struct {
  gorm.Model
  Name      string
  User      User `gorm:"foreignkey:UserRefer"` // 使用 UserRefer 作为外键
  UserRefer uint
}

var profile Profile
db.Model(&user).Related(&profile) // SELECT * FROM profiles WHERE user_id = 111; // 111 is user's ID

使用 gorm:"foreignkey:UserRefer" 标签修改外键为 UserRefer

关联外键

对于一个 belongs to 关系,通常使用所有者的主键作为外键的值,上面是 User 的 Id 字段。

当关联一个 profile 到一个 user 时,gorm 会保存 user 的 Id 到 profile 的 UserId 字段。可以使用 association_foreignkey 修改使用哪个字段。

type User struct {
  gorm.Model
  Refer string
  Name string
}

type Profile struct {
  gorm.Model
  Name      string
  User      User `gorm:"association_foreignkey:Refer"` // 设置关联外键:保存 User 的 Refer 字段到 Profile 的 UserRefer 字段
  UserRefer string
}

Has One 拥有一个

has one 也是一对一关系,一个 model 实例包含另一个 model 实例,类似 User 拥有一个 CreditCard 实例。

外键

被拥有的 model 必须存储拥有 model 的某个字段为外键,默认为 model 名加 Id,例如 UserId。

可以使用 foreignkey 改变对应的外键字段。

type User struct {
    gorm.Model
    CreditCard CreditCard `gorm:"foreignkey:UserName"`
}

type CreditCard struct {
    gorm.Model
    Number   string
    UserName string
}

关联外键

默认情况下,被拥有的 model 会使用其外键字段,保存拥有者 model 的主键,可以使用 association_foreignkey 修改。

type User struct {
    gorm.Model
    Name       `sql:"index"`
    CreditCard CreditCard `gorm:"foreignkey:uid;association_foreignkey:name"`
}

type CreditCard struct {
    gorm.Model
    Number string
    UID    string
}

查询

var card CreditCard

db.Model(&user).Related(&card, "CreditCard")
// SELECT * FROM credit_card WHERE card_refer = 1;
// 1 is user's CardRefer

Related 第二个参数 CreditCarduser 的字段名,意思是获取 userCreditCard 关联,并将获取到的数据填充到 card

如果字段名和结构体定义名相同,可以省略该参数。

Has Many 拥有多个

某个资源拥有多个另一资源的关联关系,假设 User 拥有多个 Email

Email 存储 User 的主键。

一对多时 foreignkey 存在多表里,也是用 foreignkey 标签指定。

foreignkey

表示使用多表中的哪个字段来查询关联的数据。

association_foreignkey

此时则表示,当你添加一个新的关联时,使用单表的哪个字段赋值给多表中的外键,默认是 ID

关联查询

var emails []*Email

db.Model(&user).Related(&emails)
//// SELECT * FROM emails WHERE user_id = 111; // 111 is user's primary key

多对多

一个用户可以有多个地址,一个地址可以属于多个用户。

gorm 会自动创建多对多关联表。

type User struct {
	gorm.Model
	Username string
	Mobile string
	Age int
	Active bool
	Address []Address `gorm:"many2many:user_address"`
}

CREATE TABLE `user_address` (`user_id` int unsigned,`address_id` int unsigned, PRIMARY KEY (`user_id`,`address_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

如果要修改自定义的关联表,可以直接定义一个关联表对象加入该字段去 Migration

foreignkey

和一对多一样,表示使用自己哪个字段去查关联数据。

association_foreignkey

和一对多一样。

关联查询

// User has and belongs to many languages, use `user_languages` as join table
type User struct {
  gorm.Model
  Languages         []*Language `gorm:"many2many:user_languages;"`
}

type Language struct {
  gorm.Model
  Name string
  Users         	  []*User     `gorm:"many2many:user_languages;"`
}

db.Model(&language).Related(&users)
//// SELECT * FROM "users" INNER JOIN "user_languages" ON "user_languages"."user_id" = "users"."id" WHERE  ("user_languages"."language_id" IN ('111'))

db.Model(&user).Related(&languages, "Languages")
//// SELECT * FROM "languages" INNER JOIN "user_languages" ON "user_languages"."language_id" = "languages"."id" WHERE "user_languages"."user_id" = 111

// Preload Languages when query user
db.Preload("Languages").First(&user)

关联 CRUD

创建和更新

创建和更新记录时,会自动保存关联及其引用,如果关联数据拥有主键(更新),将调用 Update 保存该数据,否则会创建。

user := User{
    Name:            "jinzhu",
    BillingAddress:  Address{Address1: "Billing Address - Address 1"},
    ShippingAddress: Address{Address1: "Shipping Address - Address 1"},
    Emails:          []Email{
        {Email: "jinzhu@example.com"},
        {Email: "jinzhu-2@example@example.com"},
    },
    Languages:       []Language{
        {Name: "ZH"},
        {Name: "EN"},
    },
}

db.Create(&user)
//// BEGIN TRANSACTION;
//// INSERT INTO "addresses" (address1) VALUES ("Billing Address - Address 1");
//// INSERT INTO "addresses" (address1) VALUES ("Shipping Address - Address 1");
//// INSERT INTO "users" (name,billing_address_id,shipping_address_id) VALUES ("jinzhu", 1, 2);
//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu@example.com");
//// INSERT INTO "emails" (user_id,email) VALUES (111, "jinzhu-2@example.com");
//// INSERT INTO "languages" ("name") VALUES ('ZH');
//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 1);
//// INSERT INTO "languages" ("name") VALUES ('EN');
//// INSERT INTO user_languages ("user_id","language_id") VALUES (111, 2);
//// COMMIT;

db.Save(&user)

跳过自动创建、自动更新

db.Set("gorm:association_autoupdate", false).Set("gorm:association_autocreate", false).Create(&user)

db.Set("gorm:association_autoupdate", false).Save(&user)
db.Set("gorm:association_autocreate", false).Create(&user)

`association_autocreate:false`
`association_save_reference:false`

关联模式

// Start Association Mode
var user User
// 必须有主键
db.Model(&user).Association("Languages")
// `user` is the source, is must contains primary key
// `Languages` is source's field name for a relationship
// AssociationMode can only works if above two conditions both matched, check it ok or not:
// db.Model(&user).Association("Languages").Error

通过关联模式可以查找关联对象,增加关联对象个数,替换关联对象,删除关联对象,清除关联对象,统计关联个数。

Find Associations

db.Model(&user).Association("Languages").Find(&languages)

Append Associations

Append new associations for many to many, has many, replace current associations for has one, belongs to

db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN})
db.Model(&user).Association("Languages").Append(Language{Name: "DE"})

Replace Associations

Replace current associations with new ones

db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN})
db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN)

Delete Associations

Count Associations

db.Model(&user).Association("Languages").Count()

预加载 Preload

预加载绝对是先进生产力,定义好模型之后,将关联的数据一个一个加载到结构体中。

// Preload preload associations with given conditions
//    db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
func (s *DB) Preload(column string, conditions ...interface{}) *DB {
	return s.clone().search.Preload(column, conditions...).db
}

开启自动预加载

db.Set("gorm:auto_preload", true)

关联查询

db.Where("state = ?", "active").Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
//// SELECT * FROM users WHERE state = 'active';
//// SELECT * FROM orders WHERE user_id IN (1,2) AND state NOT IN ('cancelled');

整体逻辑就是先找到 users 然后再填充 users 中每一个 user 的对应字段。

定义加载规则

db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
    return db.Order("orders.amount DESC")
}).Find(&users)
//// SELECT * FROM users;
//// SELECT * FROM orders WHERE user_id IN (1,2,3,4) order by orders.amount DESC;

关闭预加载

type User struct {
  gorm.Model
  Name       string
  CompanyID  uint
  Company    Company `gorm:"PRELOAD:false"` // not preloaded
  Role       Role                           // preloaded
}

db.Set("gorm:auto_preload", true).Find(&users)

Callback

Callback 顺序

db.Callback().Create().Before("gorm:create").Register("update_created_at", updateCreated)
db.Callback().Create().After("gorm:create").Register("update_created_at", updateCreated)
db.Callback().Query().After("gorm:query").Register("my_plugin:after_query", afterQuery)
db.Callback().Delete().After("gorm:delete").Register("my_plugin:after_delete", afterDelete)
db.Callback().Update().Before("gorm:update").Register("my_plugin:before_update", beforeUpdate)
db.Callback().Create().Before("gorm:create").After("gorm:before_create").Register("my_plugin:before_create", beforeCreate)

注册

注册新的 Callback 函数

func updateCreated(scope *Scope) {
    if scope.HasColumn("Created") {
        scope.SetColumn("Created", NowFunc())
    }
}

// 注册 Create 进程的回调
db.Callback().Create().Register("update_created_at", updateCreated)

删除

// 从 Create 回调中删除 gorm:create 回调
db.Callback().Create().Remove("gorm:create")

替换

// 使用新函数 newCreateFunction 替换回调 gorm:create 用于创建过程
db.Callback().Create().Replace("gorm:create", newCreateFunction)

默认定义 Callback

RowQuery 默认没有定义回调函数。在查询时调用。

func updateTableName(scope *gorm.Scope) {
  scope.Search.Table(scope.TableName() + "_draft") // append `_draft` to table name
}

db.Callback().RowQuery().Register("publish:update_table_name", updateTableName)

事务

// 开启事务
tx := db.Begin()

// 在事务中执行一些数据库操作 (从这里开始使用 'tx',而不是 'db')
tx.Create(...)

// ...

// 发生错误回滚事务
tx.Rollback()

// 或者提交这个事务
tx.Commit()

// 例子
func CreateAnimals(db *gorm.DB) err {
  // 注意在事务中要使用 tx 作为数据库句柄
  tx := db.Begin()
  defer func() {
    if r := recover(); r != nil {
      tx.Rollback()
    }
  }()

  if tx.Error != nil {
    return err
  }

  if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
     tx.Rollback()
     return err
  }

  if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
     tx.Rollback()
     return err
  }

  return tx.Commit().Error
}

源码解析

import (
  "github.com/jinzhu/gorm"
  _ "github.com/jinzhu/gorm/dialects/mysql"
)

type User struct {
    id int
    Name string
}

func main() {
    // 打开连接
    db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")

    var user User
    // 读取 id = 1 的记录
    db.First(&user, 1)
}

Open

// Open initialize a new db connection, need to import driver first, e.g:
//
//     import _ "github.com/go-sql-driver/mysql"
//     func main() {
//       db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
//     }
// GORM has wrapped some drivers, for easier to remember driver's import path, so you could import the mysql driver with
//    // import _ "github.com/jinzhu/gorm/dialects/mysql"
//    // import _ "github.com/jinzhu/gorm/dialects/postgres"
//    // import _ "github.com/jinzhu/gorm/dialects/sqlite"
//    // import _ "github.com/jinzhu/gorm/dialects/mssql"

// dialect 对应某类数据库
// SQLCommon 是一个最小化的 orm 接口
func Open(dialect string, args ...interface{}) (db *DB, err error) {
	if len(args) == 0 {
		err = errors.New("invalid database source")
		return nil, err
	}
	var source string
	var dbSQL SQLCommon
	var ownDbSQL bool

	switch value := args[0].(type) {
    case string:
        // 根据参数获取 driver 和 source
		var driver = dialect
		if len(args) == 1 {
			source = value
		} else if len(args) >= 2 {
			driver = value
			source = args[1].(string)
        }
        // 打开连接
        dbSQL, err = sql.Open(driver, source)
        // 下面是否 ping 报错的关闭条件
		ownDbSQL = true
    case SQLCommon:
		dbSQL = value
		ownDbSQL = false
	default:
		return nil, fmt.Errorf("invalid database source: %v is not a valid type", value)
	}

	db = &DB{
		db:        dbSQL,
		logger:    defaultLogger,
		callbacks: DefaultCallback,
		dialect:   newDialect(dialect, dbSQL),
	}
	db.parent = db
	if err != nil {
		return
	}
	// Send a ping to make sure the database connection is alive.
	if d, ok := dbSQL.(*sql.DB); ok {
		if err = d.Ping(); err != nil && ownDbSQL {
			d.Close()
		}
	}
	return
}

DB

无论是 Where、Or、Not、Limit、Offset… 还是其他的语句,核心逻辑都会调用 clone() 函数克隆一个新的 DB,并修改这个新 DB 中的 search 对象,返回新的 DB

因此每个 DB 都保持在某个状态不变,可以在这个状态上衍生出一个新的状态变化的 DB。

Scopes

高阶函数将某些可以复用的条件封装好调用,实现 func (*DB) *DB 函数签名。

Dialect

Scope

type Scope struct {
	Search          *search
	Value           interface{}
	SQL             string
	SQLVars         []interface{}
	db              *DB
	instanceID      string
	primaryKeyField *Field
	skipLeft        bool
	fields          *[]*Field
	selectAttrs     *[]string
}

核心函数是 DB.NewScopes(value interface{})

// NewScope create a scope for current operation
// 建立一个 SQL 执行的 Scope
func (s *DB) NewScope(value interface{}) *Scope {
	// clone 新 DB 对象
	dbClone := s.clone()
	// Value 是输出结果的对象
	dbClone.Value = value
	// clone 新的 search 对象
	return &Scope{db: dbClone, Search: dbClone.search.clone(), Value: value}
}

Callbacks

callback 函数的签名 func(scope *Scope) 就是对 Scope 对象进行操纵的函数,Scope 则是当前的操作抽象。

type Callback struct {
	creates    []*func(scope *Scope)
	updates    []*func(scope *Scope)
	deletes    []*func(scope *Scope)
	queries    []*func(scope *Scope)
	rowQueries []*func(scope *Scope)
	processors []*CallbackProcessor
}

DefaultCallback 是 gorm 默认的 Callback 对象,在里面注册了一系列 callback,按顺序调用 callback 处理一些列工作,每个 callback 只做一件事(例如读取数据库 mapping 到 struct)。

func init() {
	DefaultCallback.Query().Register("gorm:query", queryCallback)
	DefaultCallback.Query().Register("gorm:preload", preloadCallback)
	DefaultCallback.Query().Register("gorm:after_query", afterQueryCallback)
}

Kind 分为 create、update、delete、query、row_query 五种。

gorm:"" 有自己的排序规则 // sortProcessors sort callback processors based on its before, after, remove, replace

callCallbacks

参考