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
已知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
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
已知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
查询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
舰长 @圣芙蕾雅学园
粉丝