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) ,逻辑如下:

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

  • 代入Related源码中,可知我们的外键不是 结构体类型名+ID 这种形式,所以我们需要手动指定否则它猜不到。
  • 代入源码,可知随后会检测到底是 User 还是 Profile 拥有 UserDi 字段,作出区别处理。因此上述两种情况,gorm都能正确猜测并生成正确的sql语句
  • 假如, User 中还存在普通的数据字段 UserDi ,那么将导致查询出错。因为上述代码逻辑中,判断到底是谁拥有外键这一步是有先后的(即前者),因此gorm会判断 User UserDi 是外键。注意这个
  • 更复杂的情况,比如外键指向的关联外键不是主键,或者是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) 是有问题的:

  • 官网示例未指定外键字段参数,此时如同 无结构体字段的Related(...)查询 ,gorm将检测两个结构体中是否存在 结构体类型名+ID ,显然会出错
  • 外键实际上是UserDi并存在于Profile,但Profile的User字段的tag中描述了外键信息,此时我们须手动指定外键参数为 "User" (不指定的话就是走上面无结构体查询那一套,这个结构体的tag信息没有被使用)
  • 注意查询方向。反过来已知user查询profile: db.Model(&user).Related(&profile,"User") 是不行的,此时会走related的 toField != nil 逻辑,退化为 无结构体字段的Related(...)查询 ,查不到数据的
  • 假如, 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种关系:

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