gorm源代码解读

  • Post author:
  • Post category:其他


gorm官方文档http://jinzhu.me/gorm/, 之前看官网的文档,只知道怎么使用这个库,但在看到写插件的时候http://jinzhu.me/gorm/development.html#write-plugins, 不知道是如何调用回调的,所以一步步的把了解源代码的过程记录下来



预备知识golang reflect

reflect 主要用来获取运行时类型信息, 我们将通过开发一个 QueryBuilder struct来将对像映射为sql语句,执行crud, 类似于gorm的object-relation mapping packages(orm)

type QueryBuilder struct {
	Type reflect.Type
}

通常特定类型的元数据可以通过reflect.Type来获取,所以QueryBuilder只有一个成员

type Employee struct {
	ID uint32
	FirstName string
	LastName string
	Birthday time.Time
}

为了获取Employee的元数据,我们需要先对它进行实例化。

//调用Elem之前,应当先判断它的Kind()为Array, Chan, Map, Ptr, or Slice,否则Elem()抛出异常
//reflect.TypeOf(&Employee{}).Kind() == Reflect.Ptr
t := reflect.TypeOf(&Employee{}).Elem()//获取指针的底层数据的类型
builder := &QueryBuilder{Type: t}

Reflect包中的Type和Value的区别在于,Type只是获取interface{}的类型,和 Value可以获取到它的值,同时可以通过v.Type返回的Type类型,所以TypeOf只是用于在获取元数据时使用,而Value即可以获取它的元数据的值和类型

接着我们创建一个 CreateSelectQuery函数

func (qb *QueryBuilder) CreateSelectQuery() string {
	buffer := bytes.NewBufferString("")
	//应该先判断qb.Type的类型是否为struct, 否则抛出异常
	for index := 0; index < qb.Type.NumField(); index++ {
		field := qb.Type.Field(index) //StructField 描述struct单个字段的信息 

		if index == 0 {
			buffer.WriteString("SELECT ")
		} else {
			buffer.WriteString(", ")
		}
		column := field.Name
		buffer.WriteString(column)
	}

	if buffer.Len() > 0 {
		fmt.Fprintf(buffer, " FROM %s", qb.Type.Name())
	}

	return buffer.String()
}

output

SELECT ID, FirstName, LastName, Birthday FROM Employee

但我们如何来处理大小的问题和别名的问题,比如ID字段id_pk, FirstName 为first_name, LastName为last_name.

我们可以通过它field tags来实现

type Employee struct {
	ID        uint32 `orm:"id_pk"`
	FirstName string `orm:"first_name"`
	LastName  string `orm:"last_name"`
	Birthday  time.Time
}

//CreateSelectQuery
column := field.Name
if tag := field.Tag.Get("orm"); tag != "" {
	column = tag
}

buffer.WriteString(column)

接下来加入字段值的读取与较验, 假以我们有以下的struct

type PaymentTransaction struct {
	Amount      float64 `validate:"positive"`
	Description string  `validate:"max_length:250"`
}

跟上面的例子相似,我们使用tag声明。

func Validate(obj interface{}) error {
	v := reflect.ValueOf(obj).Elem()
	t := v.Type()

	for index := 0; index < v.NumField(); index++ {
		vField := v.Field(index)
		tField := t.Field(index)

		tag := tField.Tag.Get("validate")
		if tag == "" {
			continue
		}

		switch vField.Kind() {
		case reflect.Float64:
			value := vField.Float()
			if tag == "positive" && value < 0 {
				value = math.Abs(value)
				vField.SetFloat(value)
			}
		case reflect.String:
			value := vField.String()
			if tag == "upper_case" {
				value = strings.ToUpper(value)
				vField.SetString(value)
			}
		default:
			return fmt.Errorf("Unsupported kind '%s'", vField.Kind())
		}
	}

	return nil
}

reflect.Value是将interface反射为go的值。调用Kind方法,用于确定它是什么类型,然后可以通过实际的类型方法比如 (Float或String)访问它实际的值. 如果需要改变它的值,可以调用对应的setter访问

接下来,我们在将,如何调用interface{}的函数

reflect包可以用来识别是否实现某一个接口.假设我们有一个Validator接口,它提供了一个Validate函数

type Validator interface {
	Validate() error
}

而PaymentTransaction struct实现了Validator接口

func (p *PaymentTransaction) Validate() error {
	fmt.Println("Validating payment transaction")
	return nil
}

为了确定PaymentTransaction是否实现了接口,我们应该调用reflect.Type的Implements方法, 如何返回true, 表示这个类型实现了接口签名

func CustomValidate(obj interface{}) error {
	v := reflect.ValueOf(obj)
	t := v.Type()

	interfaceT := reflect.TypeOf((*Validator)(nil)).Elem()
	if !t.Implements(interfaceT) {
		return fmt.Errorf("The Validator interface is not implemented")
	}

	validateFunc := v.MethodByName("Validate")
	validateFunc.Call(nil)
	return nil
}

  1. http://blog.ralch.com/tutorial/golang-reflection/

  2. https://blog.golang.org/laws-of-reflection


    #主要的几个struct
  • type DB struct 主结构,用来管理当前的数据库连接, 它会包含下面将要列出的结构,比如Callback, Dialect
  • type Scope struct,包含每一次sql操作的信息,比如db.Create(&user), 创建一个用户时,scope包含了*DB信息,SQL信息, 字段,主键字段等
  • type Callback struct 包含所以CRUD的回调,比如beforeSave, afterSave, beforeUpdate等等,在实现我们一些自定义功能时(插件), 就需要了解这个struct
  • type Dialect interface {} 这是一个接口类型,用来实现不同数据库的相同方法,消除不同数据库要写不一样的代码,比如HasColumn(tableName string, columnName string) bool方法,在mysql的sql语句,可能跟postgres的sql语句不同,所以分别需要在dialect_mysql和dialect_postgres中实现



type DB struct

type DB struct {
	Value        interface{}  //每一次操作的对像,比如创建一个用户,删除一个用户的struct值
	Error        error
	RowsAffected int64

	// single db
	db                SQLCommon
	blockGlobalUpdate bool
	logMode           int
	logger            logger
	search            *search //保存搜索的条件where, limite, group,比如调用db.clone()时,会指定search
	values            map[string]interface{}

	// global db
	parent        *DB
	callbacks     *Callback
	dialect       Dialect
	singularTable bool
}
//DB, err = gorm.Open("mysql", fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?parseTime=True&loc=Local", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Name))
//第一个参数为方言的名称,也就是我们上面提供的Dialect类型,在dialect.go的dialectsMap变量中,保存着不同的数据库的实现方法,相于gorm中的普通话到mysql翻译, 同时,它也是原生sql.Open的第一个参数
//可以是数据库连接字符串,也可以是已有的数据库连接sql.DB
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 //一个接口类型,跟golang原生的sql.DB一样,包含Query, QueryRow, Exec, Prepare

	switch value := args[0].(type) {
	case string:
		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)  //调用原生的mysql打开数据库连接
	case SQLCommon:
		dbSQL = value //如果传入的值为*sql.DB数据库连接,直接设置为值
	}

	db = &DB{
		db:        dbSQL,
		logger:    defaultLogger,  //todo 
		values:    map[string]interface{}{},
		
		/*callback.go包含create, update, delete操作对像时的回调, 比如callback_create.go的init方法中注册BeforeCreate, 它会调用
//User为struct类型,对应user表		
func (u *User) BeforeCreate() (err error) {
    if u.readonly() {
        err = errors.New("read only user")
    }
    return
}
*/
		callbacks: DefaultCallback, 
		//注册的方言, 实现HasTable, HasColumn等标准的gorm方法
		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 {
			d.Close()
		}
	}
	return
}

##type Scope struct

包含每一个sql操作的相关信息

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

我们以在数据库中创建用户表,为例

type User struct {
	gorm.Model
	Email                  string `form:"email"`
	Password               string
	Name                   string `form:"name"`
	Gender                 string
	Role                   string
	Birthday               *time.Time
	Balance                float32
	DefaultBillingAddress  uint `form:"default-billing-address"`
	DefaultShippingAddress uint `form:"default-shipping-address"`
	Addresses              []Address
	Orders                 []Order
}
table := User{}
if err := db.DropTableIfExists(table).Error; err != nil {
	panic(err)
}
DraftDB.AutoMigrate(table)

我们先看看DropTableIfExists

func (s *DB) DropTableIfExists(values ...interface{}) *DB {
	/*
	每一个sql操作curd都会克隆出一个新的gorm.DB, 因为要绑定db.Value为新的struct对像和Search条件,只有底层的数据库连接是一样
	*/
	db := s.clone()
	for _, value := range values {
		if s.HasTable(value) {//检查这个表是否存在,s.HasTable同样会克隆当前db
			db.AddError(s.DropTable(value).Error)
		}
	}
	return db
}

func (s *DB) HasTable(value interface{}) bool {
	var (
		scope     = s.clone().NewScope(value)//克隆db,并且创建当前的会话(接着会在clone一个db, parent为s.clone())
		tableName string
	)

	if name, ok := value.(string); ok {
		tableName = name
	} else {
		tableName = scope.TableName()
	}
	//调用scope.db中的Dialect,它是我们在gorm.Open时赋值给数据库连接的, dialect在调用底层的sql.DB,执行相应的sql, 具体可以查看dialect_common.go
	has := scope.Dialect().HasTable(tableName)
	s.AddError(scope.db.Error)
	return has
}
func (s *DB) NewScope(value interface{}) *Scope {
	dbClone := s.clone()  //克隆db
	dbClone.Value = value //将user对像赋值给dbClone
	/*
	type Scope struct {
		Search          *search
		Value           interface{}
		SQL             string
		SQLVars         []interface{}
		db              *DB
		instanceID      string
		primaryKeyField *Field
		skipLeft        bool
		fields          *[]*Field
		selectAttrs     *[]string
	}
	*/
	return &Scope{db: dbClone, Search: dbClone.search.clone(), Value: value}
}
// TableName return table name
func (scope *Scope) TableName() string {
	//skip
	if scope.Search != nil && len(scope.Search.tableName) > 0 {
		return scope.Search.tableName
	}
	//skip
	if tabler, ok := scope.Value.(tabler); ok {
		return tabler.TableName()
	}
	//skip
	if tabler, ok := scope.Value.(dbTabler); ok {
		return tabler.TableName(scope.db)
	}
	
	return scope.GetModelStruct().TableName(scope.db.Model(scope.Value))
}

GetModelStruct比较复杂,主要是应用reflect解析&user的值所对应的struct类型,根据定义的tag进行相应的fieldStruct设置.

##type Callback struct

假设我们在创建用户之后,需要对用户ID进行检查

// Rollback the insertion if user's id greater than 1000
func (u *User) AfterCreate() (err error) {
    if (u.Id > 1000) {
        err = errors.New("user id is already greater than 1000")
    }
    return
}

user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
//将user作为scope的值,创建一个scope.
db.NewRecord(user) // => returns `true` as primary key is blank

db.Create(&user)

db.NewRecord(user) // => return `false` after `user` created

在db调用Create方法法,会调用scope中的callCallbacks方法,它执行db上注册的所有回调函数,这些回调函数默认的为DefaultCallback, 可以参考https://github.com/jinzhu/gorm/blob/master/callback_create.go. 以这里的AfterCreate为例, 它接着会调用defaultCallback上面的

func afterCreateCallback(scope *Scope) {
	if !scope.HasError() {
		//scope存取了当前user信息
		scope.CallMethod("AfterCreate")
	}
	if !scope.HasError() {
		scope.CallMethod("AfterSave")
	}
}

接下来看看scope的CallMethod方法, 它会解析scope中保存的值user, 然后找到AfterCreate方法,当然,它也会查找BeforeCreate,因为它也是在DefaultCallback中注册为create类型。

func (scope *Scope) callMethod(methodName string, reflectValue reflect.Value) {
	// Only get address from non-pointer
	if reflectValue.CanAddr() && reflectValue.Kind() != reflect.Ptr {
		reflectValue = reflectValue.Addr()
	}

	if methodValue := reflectValue.MethodByName(methodName); methodValue.IsValid() {
		switch method := methodValue.Interface().(type) {
		case func():
			method()
		case func(*Scope):
			method(scope)
		case func(*DB):
			newDB := scope.NewDB()
			method(newDB)
			scope.Err(newDB.Error)
		case func() error:
			scope.Err(method())
		case func(*Scope) error:
			scope.Err(method(scope))
		case func(*DB) error:
			newDB := scope.NewDB()
			scope.Err(method(newDB))
			scope.Err(newDB.Error)
		default:
			scope.Err(fmt.Errorf("unsupported function %v", methodName))
		}
	}
}

##多个表共用的callback,

我们以gorm中的l10n的源代码来说明

//l10n.RegisterCallbacks(DB)
func RegisterCallbacks(db *gorm.DB) {
	callback := db.Callback()
	//注册当前callback在gorm之前,在Register之后,所对当前db的callback进行排列,然后在调用的时候,如果调用了db.Create方法,这会执行db的callback中,所在类型为create的函数,l10n.beforeCreate, defaultCallback.beforeCreate, l10n.create defaultCallback中的create, l10n.afterCreate, defaultCallback的afterCreate.
	callback.Create().Before("gorm:before_create").Register("l10n:before_create", beforeCreate)

	callback.Update().Before("gorm:before_update").Register("l10n:before_update", beforeUpdate)
	callback.Update().After("gorm:after_update").Register("l10n:after_update", afterUpdate)

	callback.Delete().Before("gorm:before_delete").Register("l10n:before_delete", beforeDelete)

	callback.RowQuery().Before("gorm:row_query").Register("l10n:before_query", beforeQuery)
	callback.Query().Before("gorm:query").Register("l10n:before_query", beforeQuery)
}

以l10n的beforeCreate举例说明

func beforeCreate(scope *gorm.Scope) {
/* IsLocalizable 检查scope中的要保存的值e.g. &Product{},即model.Product是否实现包含了l10n中的
type Locale struct {
	LanguageCode string `sql:"size:20" gorm:"primary_key"`
}
type Product struct {
	l10n.Locale //实现l10n.l10nInterface
}
*/
	if IsLocalizable(scope) {
	//getLocale会调用DraftDB.Set("l10n:locale", "zh-CN").Save(&product),设置的l10n:locale
		if locale, ok := getLocale(scope); ok { // is locale
			if isLocaleCreatable(scope) || !scope.PrimaryKeyZero() {
				setLocale(scope, locale)
			} else {
				err := fmt.Errorf("the resource %v cannot be created in %v", scope.GetModelStruct().ModelType.Name(), locale)
				scope.Err(err)
			}
		} else {
			setLocale(scope, Global)
		}
	}
}

#创建,查询,更新,删除

	db.DropTableIfExists(&Product{})//删除数据库表
	db.DropTableIfExists(&Brand{})
	db.DropTableIfExists(&Tag{})
	db.DropTableIfExists(&Category{})
	db.Exec("drop table product_tags;")//手动删除关联子表,即没有model struct
	db.Exec("drop table product_categories;")
	db.AutoMigrate(&Product{}, &Brand{}, &Tag{}, &Category{}) //根据struct创建数据库表, 每一张表都会带有language_code, 它定义在l10n.go
	dbGlobal = db
	dbCN = dbGlobal.Set("l10n:locale", "zh")
	dbEN = dbGlobal.Set("l10n:locale", "en")
	dbGlobal.Create(&product).Error //创建一条记录
	dbGlobal.Model(&Product{}).Where("id = ? AND code = ?", product.ID, "CreateWithCreate")//构建where语名,并不查询
	db.Set("l10n:locale", locale).Count(&count);//执行查询,并先 调用beforeQuery回调函数,
	func beforeQuery(){
	scope.Search.Order(gorm.Expr(fmt.Sprintf("%v.language_code = ? DESC", quotedTableName), locale))
	scope.Search.Where(fmt.Sprintf("%v.language_code = ?", quotedTableName),locale)
	}
	product.Name = "新的中文名"
	product.Code = "NewCode // should be ignored when update"
	dbCN.Save(&product)//更新一条记录
	dbCN.Search.Omit([]string{"code","quantity", "brand_id"}) //忽略以下字段的更新
	dbGlobal.Model(&Product{}).Where("id = ?", product.ID).UpdateColumns(map[string]interface{}{"quantity": gorm.Expr("quantity + ?", 2)}) //通过全局db来更新指定的字段

#创建表 和 记录



Associations


https://gist.github.com/ShionRyuu/00385f4959884386ac72



https://ruby-china.github.io/rails-guides/association_basics.html


将实体与实体的关系,反应到最终数据库的设计上来,将关系分为:一对一,一对多,多对多

所有的关系都是指的表与表之间的关系

一对一

一张表的一条记录一定只能与另外一张表的一条记录进行对应,反之亦然。

学生表:姓名,性别,年龄,身高,体重,籍贯,家庭住址,紧急联系人

其中姓名、性别、年龄、身高,体重属于常用数据,但是籍贯、住址和联系人为不常用数据

如果每次查询都是查询所有数据,不常用的数据就会影响效率,实际又不用

常用信息表:ID§,姓名,性别,年龄,身高,体重

不常用信息表:ID§,籍贯,家庭住址,紧急联系人

解决方案:将常用的和不常用的信息分享存储,分成两张表

不常用信息表和常用信息表,保证不常用信息表与常用信息表能够对应上:找一个具有唯一性的

字段来共同连接两张表。

一个常用表中的一条记录永远只能在一张不常用表中匹配一条记录,反之亦然。

一对多

一张表中有一条记录可以对应另外一张表中的多条记录;但是反过来,另外一张表的一条记录

只能对应第一张表的一条记录,这种关系就是一对多或多对一

母亲与孩子的关系:母亲,孩子两个实体

母亲表:ID§,名字,年龄,性别

孩子表:ID§,名字,年龄,性别

以上关系:一个妈妈可以在孩子表中找到多条记录(也可能是一条),但是一个孩子只能找到一个妈妈

是一种典型的一对多的关系。

但是以上设计:解决了实体的设计表问题,但是没有解决关系问题,孩子找不到母亲,母亲也找不到孩子

解决方案:在某一张表中增加一个字段,能够找到另外一张表中的记录:在孩子表中增加一个字段

指向母亲表,因为孩子表的记录只能匹配到一条母亲表的记录。

母亲表:ID§,名字,年龄,性别

孩子表:ID§,名字,年龄,性别,母亲表ID(母亲表主键)

多对多

一对表中(A)的一条记录能够对应另外一张表(B)中的多条记录;同时B表中的一条记录

也能对应A表中的多条记录

老师和学生

老师表 T_ID§,姓名,性别

学生表 S_ID§,姓名,性别

以上设计方案:实现了实体的设计,但是没有维护实体的关系

一个老师教过多个学生,一个学生也被多个老师教过

解决方案:增加一张中间关系表

老师与学生的关系表:ID§,T_ID,S_ID

老师表与中间表形成一对多的关系,而中间表是多表;维护了能够唯一找到一表的关系;

同样的学生表与中间表也是一个一对多的关系;

学生找老师:找出学生ID—>中间表寻找匹配记录(多条)—>老师表匹配(一条)

老师找学生:找出老师ID—>中间表寻找匹配记录(多条)—>学生表匹配(一条)



Belongs To

belongs_to 关联创建两个模型之间一对一的关系,声明所在的模型实例属于另一个模型的实例。例如,如果应用中有作者和图书两个模型,而且每本书只能指定给一位作者,就要这么声明图书模型, 属于一对一

// `User` belongs to `Profile`, `ProfileID` is the foreign key 默认情况下不会在User表中保存信息, 而且以字段值+ID为外键
// 可以通过`gorm:"ForeignKey:ProfileRefer"`指定哪个字段作为外键
type User struct {
	gorm.Model
	Profile   Profile
	ProfileID int
}

type Profile struct {
	gorm.Model
	Name string
}
user := User{
		Profile: Profile{
			Name: "test",
		},
	}

db.Create(&user)

也可以指定外键为某一列

type Profile struct {
    gorm.Model
    Refer int
    Name  string
}

type User struct {
    gorm.Model
    Profile   Profile `gorm:"ForeignKey:ProfileID;AssociationForeignKey:Refer"`
    ProfileID int
}



在 belongs_to 和 has_one 之间选择

如果想建立两个模型之间的一对一关系,要在一个模型中添加 belongs_to,在另一模型中添加 has_one。但是怎么知道在哪个模型中添加哪个呢?

二者之间的区别是在哪里放置外键(外键在 belongs_to 关联所在模型对应的表中),不过也要考虑数据的语义。has_one 的意思是某样东西属于我,即哪个东西指向你。例如,说供应商有一个账户,比账户拥有供应商更合理

###Has Many

// User has many emails, UserID is the foreign key
type User struct {
    gorm.Model
    Emails   []Email
}

type Email struct {
    gorm.Model
    Email   string
    UserID  uint
}

db.Model(&user).Related(&emails)

指定外键

type Profile struct {
  gorm.Model
  Name      string
  UserRefer uint
}

type User struct {
  gorm.Model
  Profiles []Profile `gorm:"ForeignKey:UserRefer"`
}

指定外键和关联键

type Profile struct {
  gorm.Model
  Name   string
  UserID uint
}

type User struct {
  gorm.Model
  Refer   uint
  Profiles []Profile `gorm:"ForeignKey:UserID;AssociationForeignKey:Refer"`
}

###Polymorphism

关联还有一种高级形式——多态关联(polymorphic association)。在多态关联中,在同一个关联中,一个模型可以属于多个模型。例如,图片模型可以属于雇员模型或者产品模型,模型的定义如下

当前gorm只支持has-many and has-one 关系,不支持belongs-to and many-to-many

在gorm中默认的是通过 OwnerType和OwnerId来连接

type Cat struct {
    Id    int
    Name  string
    Toy   Toy `gorm:"polymorphic:Owner;"`
  }

  type Dog struct {
    Id   int
    Name string
    Toy  Toy `gorm:"polymorphic:Owner;"`
  }

  type Toy struct {
    Id        int
    Name      string
    OwnerId   int
    OwnerType string
  }

###Association Mode

Association Mode 包含许多Helper方法来处理关系

// Start Association Mode
var user User
db.Model(&user).Association("Languages")
// `user` is the source, it need to be a valid record (contains primary key)
// `Languages` is source's field name for a relationship.
// If those conditions not matched, will return an error, check it with:
// db.Model(&user).Association("Languages").Error


// Query - Find out all related associations 找到当前user相关联的数据
db.Model(&user).Association("Languages").Find(&languages)


// Append - Append new associations for many2many, has_many, will replace current association for has_one, belongs_to
db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN})
db.Model(&user).Association("Languages").Append(Language{Name: "DE"})


// Delete - Remove relationship between source & passed arguments, won't delete those arguments
db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN})
db.Model(&user).Association("Languages").Delete(languageZH, languageEN)


// Replace - Replace current associations with new one
db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN})
db.Model(&user).Association("Languages").Replace(Language{Name: "DE"}, languageEN)


// Count - Return the count of current associations
db.Model(&user).Association("Languages").Count()


// Clear - Remove relationship between source & current associations, won't delete those associations
db.Model(&user).Association("Languages").Clear()



预备知识

//gorm join_table_handler.go
type JoinTableHandler struct {
	TableName   string          `sql:"-"`
	Source      JoinTableSource `sql:"-"`
	Destination JoinTableSource `sql:"-"`
}
joinTableHandler := JoinTableHandler{}
joinTableHandler.Setup(relationship, "product_categories", Source, Destination)

生成关系表时处理器, 通常使用Setup方法, relationship包含类型, 外键字段名称,关联字段名称, 第二个参数为关联表的类型,第三个为source model类型

第四人为 distination model struct

//gorm model_struct.go
type Relationship struct {
	Kind                         string
	PolymorphicType              string
	PolymorphicDBName            string
	PolymorphicValue             string
	ForeignFieldNames            []string
	ForeignDBNames               []string
	AssociationForeignFieldNames []string
	AssociationForeignDBNames    []string
	JoinTableHandler             JoinTableHandlerInterface
}

Kind可以为many_to_many, has_many等

PolymorphicType为设置了Polymorphic时,才会设置的值, 表示的是has-one, has-many, belong-to的关系

ForeignFieldNames 保存source的主键名称

ForeignDBNames 生成关系表时的名称比如product_id

AssociationForeignFieldNames 相关表的主键名称

AssociationForeignDBNames 生成关系表时,Destination的主键dbname, 比如category_id

生成_draft表

type Product struct {
	ID         int        `gorm:"primary_key"`
	Categories []Category `gorm:"many2many:product_categories;ForeignKey:id;AssociationForeignKey:id"`  //2-j
	Brand      Brand  //参考 2->g
	locale //参考2->i
}
type Category struct {
	ID   int `gorm:"primary_key"`
	Name string
}
type Brand struct {
	ID   int `gorm:"primary_key"`
	Name string
	Locale
}
type Locale struct {
	LanguageCode string `sql:"size:20" gorm:"primary_key"`
}
func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	db := utils.TestDB()
	db.DropTableIfExists(&Product{})
	db.DropTableIfExists(&Category{})
	db.Exec("drop table product_categories;")
	//创建producs, categories表
	db.AutoMigrate(&Product{}, &Category{}, &Brand)
}

以下是普通的AutoMigrate过程

  1. 创建&Product{}, 生成表名products


  2. 遍历每一个字段_, field := range scope.GetModelStruct().StructFields, 解析过程如下

    首先解析tag

    a. 解析字段的sql, gorm tag, 并且以;分割每个gorm设置, 返回一个map

    b. 如果在gorm设置为”-“, 则field.IsIgnored = true

    c. “PRIMARY_KEY”, 则field.IsPrimaryKey= true, 同时将这个字段保存到model.PrimaryFields数组中(哪些是主键StrctField字段)

    d. “DEFAULT” tag field.HasDefaultValue = true

    e. “AUTO_INCREMENT” tag field.HasDefaultValue = true

    接下来解析model struct中字段的类型

    f. 如果字段的类型为指针,则返回它的实际类型fieldStruct.Type.Elem(), 通过reflect.New(indirectType).Interface()返回一个接口值

    g. 接口值实现了sql.Scanner接口, 比如sql.NULLString, 其它的原生类型(int, string, bool, float64)或者自定义类似都没有实现此接口,设置field.IsScanner, field.IsNormal = true, true (sql.Scanner可以参考https://golang.org/src/database/sql/sql.go),

    如果这个字段的类型同时是一个struct类型,则解析这个子struct的每一个字段的tag, 如果找到而且field.TagSettings中没有设置,则添加

    h. 如果没有实现Scanner, 则判断是否为

    time.Time, 如果是,则field.IsNormal = true

    i. 如果也不是

    time.Time, 判断是否设置了tag为”EMBEDDED”, 同时fieldStruct.Anonymous, 则调用调用递归GetModelStruct(),然后continue跳过后面的代码, 执行下一个field解析, 解析出它所有的字段, 过程如下

    1. 将当前field的名字,添加到 subField.Names中

    2. 如果field中设置了 EMBEDDED_PREFIX tag, 则subFeild在数据库表的名字为subField.DBName = prefix + subField.DBName

    3. 如果subfield.IsPrimaryKey, 则添加到当前modelStruct.PrimaryFields

    4. 如果subField.Relationship和subField.Relationship.JoinTableHandler 非nil, 即tag中设置了many2many tag。则设置一个新的JoinTableHandler = newJoinTableHandler.Setup(subField.Relationship, joinTableHandler.TableName, reflectType, joinTableHandler.Destination.ModelType)

    j. 如果字段类型是一个slice, 即这个字段包含多个值, 它使用了defer特性Last In First Out, 即从最后一个字段开始解析它的跟其它表的关系, 表示many-to-many has-many

    1. 字段是否设置了FOREIGNKEY, 如果设置了,则添加到foreignKeys数据中

    2. ASSOCIATIONFOREIGNKEY tag, 添加到associationForeignKeys

    3. 解析当前slice的值的类型

    4. 哪果字段设置了MANY2MANY, 同时slice保存的是struct类型

    1). 创建一个model_struct.go Relationship > relationship

    2). relationship.Kind = “many_to_many”

    3). 如果之前没有解析到FOREIGNKEY tag, 则将整个modelstruct中包含的主键添加到 foreignKeys数据中

    4). foreignKeys中保存的是字符串,所以我们需要通过getForeignField方法来查找它所对应的field, 如果找到field, 比如id所对应的field(这也是为什么使用LIFO的原因, 能够查找所有之前设置的field)

    将这个field的DBName添加到relationship.ForeignFieldNames数组中(source foreight keys),

    同时为source设置连接表的外键为reflectType.Name

    () +”_” +foreignField.DBName, 即product_id

    将它添加到ForeignDBNames中

    5). 如果没有找到ASSOCIATIONFOREIGNKEY,则创建一个新的toScope(scope.New(&Category)), 它的值为字段的类型(这里为Category), 它也会调用GetModelStruct()进解析, 递归,然后将它的主键添加到associationForeignKeys

    6). 类似于step 4, category中的主键添加到relationship.AssociationForeignFieldNames中, 同时每一个字段的名称为category_主键名称, 并添加到relationship.AssociationForeignDBNames中

    7). 设置新建一个JoinTableHandler, 调用它的Setup方法,第一个参数为刚刚设置的relationship, 第二个为many2many的值product_categories, 第三个参数为 Product struct, 第四个为 Category struct

    将这个JoinTableHandler设置为relationship.JoinTableHandler, 然后field.relationship = relationship

    5. 如果没有设置任何tag, 则默认为has_many, 比如User has many comments, associationType 为 User, comment 使用 UserID 作为 foreign key

    1). relationship.Kind = “has_many”, associationType=表名(默认)

    2). 如果field设置Polymorphic, 比polymorphic:Owner, 则在toFileds中找到OwnerType字段, 设置assoctionType为PLYMORPHIC标签的值

    找到字段 relationship.PolymorphicType =

    polymorphicType.Name

    (OwnerType), relationship.PolymorphicDBName = polymorphicType.DBName

    如果field中设置了POLYMORPHIC_VALUE, 则为relationship.PolymorphicValue = value, 否则为表的名字

    设置OwenType字段的IsForeignKey = true

    3). 如果没有指定foreignkey, 也没有指定associationForeignKey, 遍历当前表的主键, 所以foreignKeys为associationType(表名)+

    field.Name

    (主键),

    而associationForeignKeys为field.Name


    4). 如果没有批定foreignkey, 而指定了associationForeignKey, 则遍历associationForeignKey, 并按照上一步的方式,生成foreignKeys

    5). 指定foreignKey, 则按照上面两步的方法,生成foreignKeys,associationForeignKeys

    6). 遍历foreignKeys, 从toFields中找到foreignKey的Field作为foreignField, 在从本表中找到对应的associationField

    设置foreignField.IsForeignKey=true

    relationship.AssociationForeignFieldNames为当前字段的名字

    relationship.AssociationForeignDBNames为当前字段的数据库名字

    relationship.ForeignFieldNames为toScope中相关字段的名字

    relationship.ForeignDBNames为toScope数据库表的名字

    7) 只有在relationship.ForeignFieldNames != 0时,才设置 field.Relationship = relationship

k. 如果字段是一个struct类型,同时没有实现sql.Scanner接口, 表示has-one或者belongs to. 如果 user has one profile, associationType 为 User, profile 使用 UserID 作为 foreign key

如果user belongs to profile, associationType 为 Profile, user 使用 ProfileID 作为 foreign key, 此步聚跟j->5类似, 以之前belong to中讲过的例子为例

type Profile struct {
         gorm.Model
         Refer int
         Name  string
     }
     //belongs-to
     type User struct {
         gorm.Model
         Profile   Profile `gorm:"ForeignKey:ProfileID;AssociationForeignKey:Refer"`
         ProfileID int
     }
//has-one
 type Cat struct {
     Id    int
     Name  string
     Toy   Toy `gorm:"polymorphic:Owner;"`
 }
type Dog struct {
     Id   int
     Name string
     Toy  Toy `gorm:"polymorphic:Owner;"`
 }
type Toy struct {
     Id        int
     Name      string
     OwnerId   int
     OwnerType string
 }
     
        1. 检查聚合的struct是否包含FOREIGNKEY 标签, 如果包含,则添加到tagForeignKeys中, 然后检查是否包含ASSOCIATIONFOREIGNKEY, 添加到tagAssociationForeignKeys
        2. 检查是否设置了POLYMORPHIC标签, 如果设置了,比如Owner,则在聚合的类中(Toy)查找Owner+"Type"字段,则设置区分不同表的列为OwnerType, 即通过OwnerType来查询toy所对应的拥有者,cat or dog
           associationType变量由表名Cat改为Owner
           relationship.PolymorphicType = OwnerType
           relationship.PolymorphicDBName = owner_type,
           接着检查field中是否设置了POLYMORPHIC_VALUE
           是:relationship.PolymorphicValue=设置的值
           否:relationship.PolymorphicValue=表名(通过PolymorphicValue在PolymorphicDBName查询它相关的值, cat or dog)
        3. has-one的检查
            1).没有定义foreignKey 标签, 也没有定义associationForeignKey标签,则外键为 表名+表的主键, 比如这里为UserId 或者 CatId, DogId, OwnerId(多态), 关联键为user表的Id键 (has one)
            2).没有定义foreignKey标签,但设置了associationForeignKey, 则从associationForeignKey中生成外键, 从当前表中查找associationForeignKey字段,找到后外键为表名+Found Value, 关联键为 found field.name
            3).设置了foreignKey标签, 没有设置associationForeignKey, 外键是否以表名开头, 如果是,关联键去除掉表名, 如果在当前表中存在这个关联键,则添加到associationForeignKeys, 如果在当前表中不存在此键,则关联键为当前表的主键, associationForeignKeys > foreignKeys
            4). 即存在foreignKey也存在associationForeignKeys, 检查是否相等,如果不相等,则报错

        4. 在聚合表中查找外键(Toy)中查询外键, 这里为OwnerId, 如果找到, 则在当前表对应的关联键associationForeignKey, 如果都找到
            foreignField.IsForeignKey = true (Toy.OwnerId field)
            relationship.AssociationForeignFieldNames 设置为当前表的id
            relationship.ForeignFieldNames 则设置为Toy表的owner_id
            所以Cat的Toy字段可以描述为
            OwnerId或者CatId是Toy 模型的外键, 并且设置Toy模型(自动解析Toy模型,并缓存为gorm.ModelStruct)中OwnerId或者CatId字段为外键foreignField.IsForeignKey
            它参照的键为Cat表的Id
        5. 如果在聚合表中找到了foreignKey字段(UserId, 或者说OwnerId), 则relationship.Kind="has_one", 否则继续检查是否为belong_to,即第6步, 在我们的例子中的User, 它就不在Profile中包含UserId, 所以它不是has-one的关系
        6. belongs-to
            1). 没有找到foreignKey, 同时也没有设置associationForeignKey, 则外键为当前字段名字+toScope的主键, ProfileId, associationForeignKey为Profile的Id
            2). 没有找到foreignKey, 但设置了associationforeignKey, 则在Profile表中查找 "Refer"字段, 如果找到,则外键为当前字段名+Refer, 即ProfileRefer, associationForekgnKeys为Refer, len(associationForeignKeys) > len(foreignKeys)
            3). 设置了foreignKey, 没有设置associationForeignKey, (ProfileId), 先删除Profile, -> Id, 接着在Profile struct中查找id, 找到了,则赋值给associationForeignKeys
            4). 同时设置了foreignKey, associationforeignKey, 检查是否一样
        7. 在当前表中查找foreignKey, 这里为ProfileId, 然后在聚合表Profile中查找associationForeignKey, 如果找到,则
            当前字段为外键, User.Profile.IsForeignKey = true
            它的关系描术为,User表的(ForeignFieldName ProfileId)参考Profile(AassociationForeignFieldName 的 id或refer字段, distination)
        8. 不管是has-one or belong-to,  它们包含ForeignFieldNames为外键的字段名,相关联的AssociationForeignDBNames(参考)字段的名称,以及当前字段IsForeignKey是否为true, 这三个值
           如果包含polymorphic,则还包含polymorphicType字段,用于区分不同的表, 而它并不会向many2many 那样,加入JoinTableHandler, 所以在生成表时,不会生成关系表

    l. 常规值(int, string),设置field.IsNormal=true
    m. 缓存当前解析的struct
3. scope.CreateTable 会遍历每一个当前表的field, 如果这个field主键,则添加到primaryKeys, 如果这个字段有relationship,则生成关系表, 全部字段处理完成之后,才生成当前数据库表, 以下是createJoinTable的处理方式
    a. 字段有Relationship, 并且relationship.JoinTableHandler不为nil
    b. 通过joinTableHandler可以获取到关系表的表名, 赋值给,并判断是否存在于数据库中
    c. 根据当前字段类型,创建一个toScope
    d. 遍历relationship中的ForeignFieldNames字段, 获取这些字段的名称,然后在当前scope中查找这些字段,然后添加到sqlTypes和primaryKeys
    e. 同理,在toScope中找到relationship.AssociationForeignFieldNames指定的键,也添加到sqlTypes, primaryKeys
    f. 调用原生sql, 将sqlTypes, primaryKeys传入给它, 创建joinTable表
    g. 调用scope.NewDB().Table(joinTable).AutoMigrate(joinTableHandler),此句可以不需要, GetModelStruct并不能解析joinTableHandler成数据库表

##创建

Create and save都会调用, 默认的DefaultCallback

//对于作何创建,db.Create(&Product{})都开启一个事务, 回调函数位于callback_save.go文件, 并且将事务保存进scope.db.Db
DefaultCallback.Create().Register("gorm:begin_transaction", beginTransactionCallback)
DefaultCallback.Create().Register("gorm:before_create", beforeCreateCallback)   //位于callback_create.go, 它执行Product中定义的BeforeSave或者BeforeCreate方法
/*
更新或者创建时调用, 确认是否先保存关联表
先判断是否在scope中设置了gorm:save_associations
比如db.UpdateColumns会将Set("gorm:save_associations", false), 就是更新某些字段时不需要保存关联表数据, 只更新当前表, 通常这个值没有设置,所以默认为true
/*
举例来说 association_test.go

func TestSkipSaveAssociation(t *testing.T) {
	type Company struct {
		gorm.Model
		Name string
	}

	type User struct {
		gorm.Model
		Name      string
		CompanyID uint
		Company   Company `gorm:"save_associations:false"`
	}
	DB.AutoMigrate(&Company{}, &User{})

	DB.Save(&User{Name: "jinzhu", Company: Company{Name: "skip_save_association"}})
    //正常来说应该会在company中保存一条记录,但这里使用save_associations:false, 则直接跳过
	if !DB.Where("name = ?", "skip_save_association").First(&Company{}).RecordNotFound() {
		t.Errorf("Company skip_save_association should not been saved")
	}
}
注:tag的设置并不影响scope.Get("gorm:save_associations")
*/

以下是详细的方法
func saveBeforeAssociationsCallback(scope *Scope) {
	if !scope.shouldSaveAssociations() { //检查是否设置了gorm:save_associations
		return
	}
    //scope保存了value 为&User{Profile: Profile{name:"hello"}}
    //scope.Fields会调用scope.GetModelStruct()解析model的字段
    //在调用reflect.Indirect(scope.IndirectValue).FieldByName来查询 User中对应字段的值
	for _, field := range scope.Fields() {
	//saveFieldAsAssociation查看是否设置了SAVE_ASSOCIATIONS标签为skip or false, 如果是,则返回false
	//saveFieldAsAssociation同时类型为belongs_to,则先创建Profile的值
		if ok, relationship := saveFieldAsAssociation(scope, field); ok && relationship.Kind == "belongs_to" {

		    //聚合model的值, profile
			fieldValue := field.Field.Addr().Interface()
			//保存Profile的值
			scope.Err(scope.NewDB().Save(fieldValue).Error)

			//设置当前表的外键值 ProfileId
			if len(relationship.ForeignFieldNames) != 0 {
				// set value's foreign key
				for idx, fieldName := range relationship.ForeignFieldNames {
				    //查找relationship中跟foreignFieldNames外键相对应的associationForeignKey的列名, 这里为Profile表中的id
					associationForeignName := relationship.AssociationForeignDBNames[idx]
					//User中Profile查找id的值, 然后设置User外键的值ProfileId
					if foreignField, ok := scope.New(fieldValue).FieldByName(associationForeignName); ok {
						scope.Err(scope.SetColumn(fieldName, foreignField.Field.Interface()))
					}
				}
			}
		}
	}
}
*/
DefaultCallback.Create().Register("gorm:save_before_associations", saveBeforeAssociationsCallback)//位于callback_save.go文件,


//如果models中有CreateAt或者UpdateAt字段,则设置它们的值
DefaultCallback.Create().Register("gorm:update_time_stamp", updateTimeStampForCreateCallback)


/*
首先遍历所有的字段, 如果字段是需要更新的
	for _, field := range scope.Fields() {
			if scope.changeableField(field) {
                //需要保存到当前表的字段,可以查看model_struct.GetModelStruct, 通常为int, string, bool, float等正常的字段
				if field.IsNormal {
                    //scopes.Fields()会为每一个Field设置值,如果这个Field的值是零值"", false, 0,则IsBlank为true
                    //GetModelStruct解析model时,如果字段包含了default或者auto_increment, 则HasDefaultvalue = true
                    //所以当这两个条件满足时,需要在数据保存到数据库后,重新设置这些列的值为默认值或者auto_increment,
                    //详细了解 参考forceReloadAfterCreateCallback
					if field.IsBlank && field.HasDefaultValue {
						blankColumnsWithDefaultValue = append(blankColumnsWithDefaultValue, scope.Quote(field.DBName))
						scope.InstanceSet("gorm:blank_columns_with_default_value", blankColumnsWithDefaultValue)
					} else if !field.IsPrimaryKey || !field.IsBlank {

						columns = append(columns, scope.Quote(field.DBName))
						placeholders = append(placeholders, scope.AddToVars(field.Field.Interface()))
					}
				} else if field.Relationship != nil && field.Relationship.Kind == "belongs_to" {

					for _, foreignKey := range field.Relationship.ForeignDBNames {
						if foreignField, ok := scope.FieldByName(foreignKey); ok && !scope.changeableField(foreignField) {
							columns = append(columns, scope.Quote(foreignField.DBName))
							placeholders = append(placeholders, scope.AddToVars(foreignField.Field.Interface()))
						}
					}
				}
			}
		}
执行sql语句
*/
DefaultCallback.Create().Register("gorm:create", createCallback)
//通过db.Select选择createCallback中设置的从数据库中有默认值,而当前scope中字段值为空的例, 然后赋值给当前scope
/*
if blankColumnsWithDefaultValue, ok := scope.InstanceGet("gorm:blank_columns_with_default_value"); ok {
        //blank_columns_with_default_value中保存的列
		db := scope.DB().New().Table(scope.TableName()).Select(blankColumnsWithDefaultValue.([]string))
		for _, field := range scope.Fields() {
		    //field主键,常见的为id, 它会在createCallback中,设置为lastInsertId
			if field.IsPrimaryKey && !field.IsBlank {
				db = db.Where(fmt.Sprintf("%v = ?", field.DBName), field.Field.Interface())
			}
		}
		db.Scan(scope.Value)
	}
*/
DefaultCallback.Create().Register("gorm:force_reload_after_create", forceReloadAfterCreateCallback)
//它跟saveBeforeAssociationsCallback (主要用于belong_to关系) 相反,saveAfterAssociationsCallback用于has_one, has_many, many_to_many的更新
DefaultCallback.Create().Register("gorm:save_after_associations", saveAfterAssociationsCallback)
//调用model中设置的AfterCreate和AfterSave方法
DefaultCallback.Create().Register("gorm:after_create", afterCreateCallback)
//提交回调,如果有错误发生,则回滚事务
DefaultCallback.Create().Register("gorm:commit_or_rollback_transaction", commitOrRollbackTransactionCallback)

##JoinTableHandler

当一个struct中以product为例 包含

gorm:"many2many:product_tags"

type Product struct {
	ID         int        `gorm:"primary_key"`
	Categories []Category `gorm:"many2many:product_categories;ForeignKey:id;AssociationForeignKey:id"`
}
type Category struct {
	ID   int `gorm:"primary_key"`
	Name string
}

func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	db := utils.TestDB()
	db.DropTableIfExists(&Product{})
	db.DropTableIfExists(&Category{})
	db.Exec("drop table product_categories;")
	//创建producs, categories表, 如果product实现了tabler接口,有TableName方法,则直接调用这个方法 
	//否则调用model_struct的TableName方法,它在调用inflection.Plural就将product变成products
	//在调用autoMigrate方法,在调用scope的createTable, 遍历这个struct包含的每一个字段,然后调用createJoinTable(field),如果field包含many2many则,生成相对应的关系表(表的名字在GetModelStruct解析many2many tag)
	db.AutoMigrate(&Product{}, &Category{})
}

它将生成

product_categories表

###为表添加后缀_draft

正常是根据db中是否设置了draft标识,直接返回_draft表,但对于many2many多表关系时,也需要加上_draft,

即product_categories_draft

tableHandler := gorm.DefaultTableNameHandler //默认直接返回tableName
	gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultName string) string {
		tableName := tableHandler(db, defaultName)  //调用gorm中定义的DefaultTableNameHandler
		//自定义model struct对应的表名字
		if db != nil {
			//db.Value为设置了model的值实现了publishInterface
			if IsPublishableModel(db.Value) {
				typ := utils.ModelType(db.Value)
				//如果没有缓存此model, 因injectedJoinTableHandler为bool值,如果没有找到,则为零值false
				if !injectedJoinTableHandler[typ] {
					injectedJoinTableHandler[typ] = true //设置缓存
					scope := db.NewScope(db.Value)
					for _, field := range scope.GetModelStruct().StructFields {
						//如果包含了many2many的字段,我们需要先创建关联表
						if many2many := utils.ParseTagOption(field.Tag.Get("gorm"))["MANY2MANY"]; many2many!= "" {
							//调用SetJoinTableHandler, 找到此例,比如此例的名称为 Categories []Category `gorm:"many2many:product_categories"`
							//source为 Product
							//destination为Category
							//调用publishJoinTableHandler.Setup, 它的第一个参数为field.Relationship, relationship是通过scope.GetModelStruct()获得
							//第二个参数many2many, product_categories, 第三个参数为source, 第四个为destination
							//如果db设置了public_draft, 则publishJoinTableHandler返回product_categories_draft
							//但没有必要在SetJoinTableHandler方法中调用s.Table(table).AutoMigrate(handler)
							db.SetJoinTableHandler(db.Value, field.Name, &publishJoinTableHandler{})
							//更新表
							db.AutoMigrate(db.Value)
						}
					}

				}

				var forceDraftTable bool
				if forceDraft, ok := db.Get("publish:force_draft_table"); ok {
					if forceMode, ok := forceDraft.(bool); ok && forceMode {
						forceDraftTable = true
					}
				}

				if IsDraftMode(db) || forceDraftTable {
					return DraftTableName(tableName)
				}
			}
		}
		return tableName
	}

我们知道,db.AutoMigrate(&Product{}), 它会调用到model_struct.go的TableName方法,这个方法会在最后的位置调用DefaultTableNameHandler, DefaultTableNameHandler通常直接返回传入的表名,不做作何处理,而在这里,我们需要对publish类型的DB调用AutoMigrate返回 _draft等后缀, 就需要修改gorm.DefaultTableNameHandler函数,对于many2many表,我们需要调用db.SetJoinTableHandler方法,它会调用重新修改数据库模型所对应的字段relationship, 即将publishJoinTableHandler绑定到field.relationship.JoinTableHandler = publishJoinTableHandler, 这样就可以调用publisJoinTableHandler.Table()返回many2many的关系表的表名

#全局变量

db变量的设置与scope变量的设置

//设置db变量,它将返回一个克隆的db, 然后保存在新db.Values, 这个变量可以用于callbacks. 因为之后的clone都会复制这个值, 但并不影响当前的db,这是跟InstantSet的区别
DB.Set("publish:draft_mode", true)
func (s *DB) Set(name string, value interface{}) *DB {
	return s.clone().InstantSet(name, value)
}
//只影响到当前的db
func (s *DB) InstantSet(name string, value interface{}) *DB {
	s.values[name] = value
	return s
}
// Set set value by name, scope.NewDB().Save(fieldValue).Error) 因为它不绑定scope instance, 所以对scope.db都有效
func (scope *Scope) Set(name string, value interface{}) *Scope {
	scope.db.InstantSet(name, value)
	return scope
}
// InstanceID get InstanceID for scope
func (scope *Scope) InstanceID() string {
	if scope.instanceID == "" {
		scope.instanceID = fmt.Sprintf("%v%v", &scope, &scope.db)
	}
	return scope.instanceID
}
// InstanceSet 设置当前会话操作变量,不能用于某些回调函数,比如saving associations callback
// 在saveBeforeAssociationsCallback中,对于创建的关系表,它所对应的scope已经是一个新的值,所以对它来说,scope.InstanceSet设置的值无效
func (scope *Scope) InstanceSet(name string, value interface{}) *Scope {
	return scope.Set(name+scope.InstanceID(), value)
}



has on 与 belongs_to

/*
	belongs_to  文章只有一个tag, 而一个tag可以是多个文章中使用
	type Article struct {
		ID int
		Tag
		TagID
	}
	type Tag struct {
		ID int
		Name string
	}

	has_one  每个文章都有一条Tag, 每一条tag属于一个文章
	type Article struct {
		ID int
		Tag
	}
	type Tag struct {
		ID int
		ArticleId
		Name string
	}
	1. 没有指定foreign key和association key, foreignkey 为主Model名称+主键(ArticleID), assocication key为Article的 ID键, 即关联表Tag中的ArticlePrimaryNum参考Article中的ID字段

	type Article struct {
		ID int
		PrimaryNum
		Tag `gorm:"association_foreignkey:PrimaryNum"`
		TagID
	}
	type Tag struct {
		ID int
		Name string
	}
	2. 没有指定foreign key,而在tag中指定了association key, 则从当前model中查找到 association中指定的字段(Article的ID字段), 并设置foreighKey中的ArticlePrimaryNum
	association key为 PrimaryNum,  即关联表Tag中的ArticlePrimaryNum参考Article的PrimaryNum

	type Article struct {
		ID int
		PrimaryNum
		Tag `gorm:"foreign_key:TagID"`
		TagID
	}
	type Tag struct {
		ID int
		Name string
	}
	3. 设置了foreign key, 而没有设置association key, 当foreign key的tag设置以Model名字开头, Article.PrimaryNum,
	则association key为设置的 foreign key的 PrimaryNum, 并在Article中查找, 如果在Article中没有找到这个字段, associationForeignKeys设置为当前的主键ID
	4. 两个都设置了

	## Next
	在关联表中查找foreign key (Tag中查找指定的外键)
	如果找到,则在当前表中查找associationForkeyKey的字段,并且找到
	创建relationship, 它的关系为has one的关系,  foreign key ArticleId (Tag表中的字段), Association key为 (Article表中的ID)
	否则创建realtionship 为belongs_to, foreign key 为(Article 表中指定的键,而Association eky 为Tag表中指定的键)

 */



举一个实际的例子

//支付表
type Charge struct {
	Cid string `gorm:"PRIMARY_KEY"`//charge id
	Mid string //会员ID
	Pid string //product id
	Product product.Product  `gorm:"ForeignKey:Pid"`
}
type Product struct {
	Pid string `gorm:"PRIMARY_KEY"`
	PType int 
	Name string 
	Rules string 
	UnitPrice float64
}
db.Model(&charge.Charge{}).Where("mid=?", user.Mid).Preload("Product").Find(&charges)

通常我们想要找到Charge以及支付了哪个产品, 底层的 sql为以下的内容

  SELECT * FROM `charge`  WHERE (mid='4b16ab3e')
  SELECT * FROM `price`  WHERE (`pid` IN ('1233','3333'))     

因为设置了Foreign Key 为PID符合上面的条件 3, 但pid不是以Charge.pid开头,associationForeignKeys为Charge的主键, cid. 接下来在Product中找到了Pid字段,所以表关系为has-one,但应为没有设置association foreign key, 默认设置为cid, 所以 pid为 cid的值

接下来,我们将条件改一样

Product product.Product  `gorm:"ForeignKey:Cid"`  //belong_to
SELECT * FROM `charge`  WHERE (mid='4b16ab3e') 
SELECT * FROM `price`  WHERE (`pid` IN ('1233','3333'))  

关系变为了belongs_to

foreign key 为 Charge的CID, 关联字段为Product的PID

Product product.Product  `gorm:"ForeignKey:Pid;AssociationForeignKey:Pid"`  //正确的定义方式

整个关系还是Has-one的关系,即先获取支付中的记录,正常数据库中应该保存CID,然后根据这个CID去查寻相应的产品记录。但应该我们设置了AssociationForeignKey为Pid, 所以在查找Product时,是限定为Charge记录中的Pid(产品id)而不是cid. 所以数据库中依然是产品的id.

 SELECT * FROM `charge`  WHERE (mid='4b16ab3e') 
SELECT * FROM `price`  WHERE (`pid` IN ('1','2'))  



版权声明:本文为cexo425原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。