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
}
- 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过程
- 创建&Product{}, 生成表名products
-
遍历每一个字段_, 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'))