【Go】gorm框架关联查询

10,515 阅读7分钟

前言

假定已经熟读官网文档,并且在学习中总是invalid association []报错,那么请看下文

Related 函数

名词

  1. 主表和从表: 有外键的那个叫从表
  2. 关联外键:主表中被从表外键指向的唯一字段(一般就是主键,当然索引字段也行)

sql怎样外键查询

外键字段在数据库中是实际存在的字段,如同普通字段一样存在,一般而言区别只是在于外键存在修改限制。在写sql语句时,和普通的条件查询一样,填入从表外键值去查询主表主键值,或者反过来罢了,对sql查询语句来说外键与否别无区别。因此gorm框架中,只要弄明白它怎样识别主从表和外键即可思路通畅。

函数实现略读

最终各个关联相关的函数均会调用:

func (scope *Scope) related(value interface{}, foreignKeys ...string) *Scope {
	toScope := scope.db.NewScope(value)
	tx := scope.db.Set("gorm:association:source", scope.Value)

	for _, foreignKey := range append(foreignKeys, toScope.typeName()+"Id", scope.typeName()+"Id") {
		fromField, _ := scope.FieldByName(foreignKey)
		toField, _ := toScope.FieldByName(foreignKey)

		if fromField != nil {
			if relationship := fromField.Relationship; relationship != nil {
				if relationship.Kind == "many_to_many" {
					joinTableHandler := relationship.JoinTableHandler
					scope.Err(joinTableHandler.JoinWith(joinTableHandler, tx, scope.Value).Find(value).Error)
				} else if relationship.Kind == "belongs_to" {
					for idx, foreignKey := range relationship.ForeignDBNames {
						if field, ok := scope.FieldByName(foreignKey); ok {
							tx = tx.Where(fmt.Sprintf("%v = ?", scope.Quote(relationship.AssociationForeignDBNames[idx])), field.Field.Interface())
						}
					}
					scope.Err(tx.Find(value).Error)
				} else if relationship.Kind == "has_many" || relationship.Kind == "has_one" {
					for idx, foreignKey := range relationship.ForeignDBNames {
						if field, ok := scope.FieldByName(relationship.AssociationForeignDBNames[idx]); ok {
							tx = tx.Where(fmt.Sprintf("%v = ?", scope.Quote(foreignKey)), field.Field.Interface())
						}
					}

					if relationship.PolymorphicType != "" {
						tx = tx.Where(fmt.Sprintf("%v = ?", scope.Quote(relationship.PolymorphicDBName)), relationship.PolymorphicValue)
					}
					scope.Err(tx.Find(value).Error)
				}
			} else {
				sql := fmt.Sprintf("%v = ?", scope.Quote(toScope.PrimaryKey()))
				scope.Err(tx.Where(sql, fromField.Field.Interface()).Find(value).Error)
			}
			return scope
		} else if toField != nil {
			sql := fmt.Sprintf("%v = ?", scope.Quote(toField.DBName))
			scope.Err(tx.Where(sql, scope.PrimaryKeyValue()).Find(value).Error)
			return scope
		}
	}

	scope.Err(fmt.Errorf("invalid association %v", foreignKeys))
	return scope
}

如官网中的查询:db.Model(&user).Related(&profile),逻辑如下:

  1. 从scope和toStope中读取结构体名字并拼接上ID(即最终为UserIDProfileID),和参数foreignKeys组成新的数组进行遍历
  2. 检查两个结构体中是否存在上述字段,如果存在则当作外键,分情况进行查询
  3. fromField.Relationship 一般是字段类型为结构体时才存在
  4. 嵌套的结构体不会自动查询(测试时跟官网所说有出入😂)

无结构体字段的Related(...)查询

为了突出外键,假定以下结构体:

type User struct {
	ID int
	Name string
}
type Profile struct {
	ID int
	Name      string
	UserDi int // 注意,这里是Di,不是ID,为了展示如何对应自定义外键名称
}

已知user查对应profile:db.Model(&user).Related(&profile,"UserDi")
已知profile查对应user:db.Model(&profile).Related(&user,"UserDi")

  1. 代入Related源码中,可知我们的外键不是结构体类型名+ID这种形式,所以我们需要手动指定否则它猜不到。
  2. 代入源码,可知随后会检测到底是User还是Profile拥有UserDi字段,作出区别处理。因此上述两种情况,gorm都能正确猜测并生成正确的sql语句
  3. 假如,User中还存在普通的数据字段UserDi,那么将导致查询出错。因为上述代码逻辑中,判断到底是谁拥有外键这一步是有先后的(即前者),因此gorm会判断UserUserDi是外键。注意这个
  4. 更复杂的情况,比如外键指向的关联外键不是主键,或者是many to many这种需要中间表的特殊情况,这种结构体无法进行查询。因为此时他们还未涉及到gorm的结构体tag,非结构体字段fromField.Relationship均为nil,无法对应这些复杂情况

有结构体字段的Related(...)查询

belongs to

type User struct {
	ID int
	Name string
}
type Profile struct {
	ID int
	Name      string
	UserDi int // 注意,这里是Di,不是ID,为了展示如何对应自定义外键名称
	User *User `gorm:"foreignkey:UserDi"` // 指针与否并不影响查询(注意指针问题)
}

已知profile查对应user:db.Model(&profile).Related(&user,"User")
这个即官网所说的belongs_to(profile属于user),注意这里应该有坑,我测试时官网示例db.Model(&user).Related(&profile)是有问题的:

  1. 官网示例未指定外键字段参数,此时如同 无结构体字段的Related(...)查询 ,gorm将检测两个结构体中是否存在结构体类型名+ID,显然会出错
  2. 外键实际上是UserDi并存在于Profile,但Profile的User字段的tag中描述了外键信息,此时我们须手动指定外键参数为"User"(不指定的话就是走上面无结构体查询那一套,这个结构体的tag信息没有被使用)
  3. 注意查询方向。反过来已知user查询profile:db.Model(&user).Related(&profile,"User")是不行的,此时会走related的toField != nil逻辑,退化为 无结构体字段的Related(...)查询 ,查不到数据的
  4. 假如,User中还存在普通的数据字段UserDi,那么将导致查询出错。因为我们在tag中标明了外键为"UserDi",类似之前说过的,gorm看看前后哪个结构体存在这个字段,从而判断到底是belongs to还是has one/many

has one/many

has one和has many区别不大,把最终查询的对象改成结构体切片(数组)形式即可

type User struct {
	ID int
	Name string
	Profile *Profile `gorm:"foreignkey:UserDi"` // 指针与否并不影响查询(注意指针问题)
}
type Profile struct {
	ID int
	Name      string
	UserDi int // 注意,这里是Di,不是ID,为了展示如何对应自定义外键名称
}

已知user查对应profile:db.Model(&user).Related(&profile,"Profile")
注意此时在User结构体加了Profile字段,与上面belongs to注意对比

如何区别belongs to和has one/many

看出发点,如上面User对应主表,Profile对应从表,外键始终在Profile中

无结构体字段的Related(...)查询 :没有这种区分,对他来说没有意义,根据谁查谁都没问题

有结构体字段的Related(...)查询 :已知profile(从表),查询它所属于的user(主表),就是belongs to;已知user,查询它拥有的profile,就是has one/many

Preload 查询

Preload用于一步到位,填充结构体及其嵌套的结构体字段

type User struct {
	ID int
	Name string
	Profile *Profile `gorm:"foreignkey:UserDi"` // 指针与否并不影响查询(注意指针问题)
}
type Profile struct {
	ID int
	Name      string
	UserDi int // 注意,这里是Di,不是ID,为了展示如何对应自定义外键名称
}

查询ID=1的user+查找对应Profile+填入User.Profile字段:

db.Find(&user,1)
db.Model(&user).Related(&profile,"Profile")
user.Profile = &profile

默认情况下,gorm只会查询单个表,也就是只会填充单个结构体。如果想同时填充嵌套的结构体,改用Preload方法:

db.Preload("Profile").Find(&user,1)

结构体多层嵌套时用.进行字段分割,详细参考官网示例不再重复

注:请确保外键逻辑正确。另外它不会循环查询(即假如User有Profile字段、Profile又有User字段),而全局db.Set("gorm:auto_preload", true)会导致循环查询。且预加载不调用Related函数

Association 查询

Association函数返回*Association,用于方便的修改关联关系

db.Find(&user,1)
db.Model(&user).Association("Profile").Clear()

生成的sql就是 UPDATE `profiles` SET `user_di` = NULL WHERE (`user_di` = 1)

更多例子和api参考官网示例不再重复

注:请确保外键逻辑正确,会调用Related函数

思想

不同于E-R图设计数据库,orm更关注“拥有、属于”关系,因为这个东西会实际影响查询时sql的组成,传统的1对1、1对多、多对1、多对多思想不完全适用。

从gorm源码的判断可知,实际上区别较大的只有3种关系:

  1. belongs to:1对1时,一个Profile属于一个User,即根据Profile查询对应User;多对1时,多个Profile属于一个User,但最终查询时往往仍会是查询某一个Profile对应的User。他们的sql编写并无太大差异,因此归为一类
  2. has one/many:1对1时,一个User拥有一个Profile,即根据User查询对应Profile;1对多时,一个User拥有多个Profile,仍是根据User查询Profile。gorm中区别在于传入的Profile对象是否为切片(数组)形式,sql编写并无太大差异,因此归为一类
  3. many to many:就是上面情况的结合体,详情略