gfORM没有采用其他ORM常见的BelongsTo, HasOne, HasMany, ManyToMany这样的模型关联设计,这样的关联关系维护较繁琐,例如外键约束、额外的标签备注等,对开发者有一定的心智负担。因此gf框架不倾向于通过向模型结构体中注入过多复杂的标签内容、关联属性或方法,并一如既往地尝试着简化设计,目标是使得模型关联查询尽可能得易于理解、使用便捷。

接下来关于gf ORM提供的模型关联实现,从GF v1.13.6版本开始提供,目前属于实验性特性。

那么我们就使用一个例子来介绍gf ORM提供的模型关联吧。

数据结构

为简化示例,我们这里设计得表都尽可能简单,每张表仅包含3-4个字段,方便阐述关联关系即可。

# 用户表
CREATE TABLE `user` (
  uid int(10) unsigned NOT NULL AUTO_INCREMENT,
  name varchar(45) NOT NULL,
  PRIMARY KEY (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

# 用户详情
CREATE TABLE `user_detail` (
  uid  int(10) unsigned NOT NULL AUTO_INCREMENT,
  address varchar(45) NOT NULL,
  PRIMARY KEY (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

# 用户学分
CREATE TABLE `user_scores` (
  id int(10) unsigned NOT NULL AUTO_INCREMENT,
  uid int(10) unsigned NOT NULL,
  score int(10) unsigned NOT NULL,
  course varchar(45) NOT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

数据模型

根据表定义,我们可以得知:

  1. 用户表与用户详情是1:1关系。
  2. 用户表与用户学分是1:N关系。
  3. 这里并没有演示N:N的关系,因为相比较于1:N的查询只是多了一次关联、或者一次查询,最终处理方式和1:N类似。

那么Golang的模型可定义如下:

// 用户表
type EntityUser struct {
    Uid  int    `orm:"uid"`
    Name string `orm:"name"`
}
// 用户详情
type EntityUserDetail struct {
    Uid     int    `orm:"uid"`
    Address string `orm:"address"`
}
// 用户学分
type EntityUserScores struct {
    Id     int    `orm:"id"`
    Uid    int    `orm:"uid"`
    Score  int    `orm:"score"`
    Course string `orm:"course"`
}
// 组合模型,用户信息
type Entity struct {
    User       *EntityUser
    UserDetail *EntityUserDetail
    UserScores []*EntityUserScores
}

其中,EntityUser, EntityUserDetail, EntityUserScores分别对应的是用户表、用户详情、用户学分数据表的数据模型。Entity是一个组合模型,对应的是一个用户的所有详细信息。

数据写入

写入数据时涉及到简单的数据库事务即可。

err := db.Transaction(func(tx *gdb.TX) error {
    r, err := tx.Table("user").Save(EntityUser{
        Name: "john",
    })
    if err != nil {
        return err
    }
    uid, err := r.LastInsertId()
    if err != nil {
        return err
    }
    _, err = tx.Table("user_detail").Save(EntityUserDetail{
        Uid:     int(uid),
        Address: "Beijing DongZhiMen #66",
    })
    if err != nil {
        return err
    }
    _, err = tx.Table("user_scores").Save(g.Slice{
        EntityUserScores{Uid: int(uid), Score: 100, Course: "math"},
        EntityUserScores{Uid: int(uid), Score: 99, Course: "physics"},
    })
    return err
})

数据查询

单条数据记录

查询单条模型数据比较简单,直接使用Scan方法即可,该方法会自动识别绑定查询结果到单个对象属性还是数组对象属性中。例如:

// 定义用户列表
var user Entity
// 查询用户基础数据
// SELECT * FROM `user` WHERE `name`='john'
err := db.Table("user").Scan(&user.User, "name", "john")
if err != nil {
    return err
}
// 查询用户详情数据
// SELECT * FROM `user_detail` WHERE `uid`=1
err := db.Table("user_detail").Scan(&user.UserDetail, "uid", user.User.Uid)
// 查询用户学分数据
// SELECT * FROM `user_scores` WHERE `uid`=1
err := db.Table("user_scores").Scan(&user.UserScores, "uid", user.User.Uid)

该方法在之前的章节中已经有介绍,因此这里不再赘述。

多条数据记录

查询多条数据记录并绑定数据到数据模型数组中,需要使用到ScanList方法,该方法会需要用户指定结果字段与模型属性的关系,随后底层会遍历数组并自动执行数据绑定。例如:

// 定义用户列表
var users []Entity
// 查询用户基础数据
// SELECT * FROM `user`
err := db.Table("user").ScanList(&users, "User")
// 查询用户详情数据
// SELECT * FROM `user_detail` WHERE `uid` IN(1,2)
err := db.Table("user_detail").
       Where("uid", gdb.ListItemValuesUnique(users, "User", "Uid")).
       ScanList(&users, "UserDetail", "User", "uid:Uid")
// 查询用户学分数据
// SELECT * FROM `user_scores` WHERE `uid` IN(1,2)
err := db.Table("user_scores").
       Where("uid", gdb.ListItemValuesUnique(users, "User", "Uid")).
       ScanList(&users, "UserScores", "User", "uid:Uid")

这其中涉及到两个比较重要的方法:

1. ScanList

方法定义:

// ScanList converts <r> to struct slice which contains other complex struct attributes.
// Note that the parameter <listPointer> should be type of *[]struct/*[]*struct.
// Usage example:
//
// type Entity struct {
// 	   User       *EntityUser
// 	   UserDetail *EntityUserDetail
//	   UserScores []*EntityUserScores
// }
// var users []*Entity
// or
// var users []Entity
//
// ScanList(&users, "User")
// ScanList(&users, "UserDetail", "User", "uid:Uid")
// ScanList(&users, "UserScores", "User", "uid:Uid")
// The parameters "User"/"UserDetail"/"UserScores" in the example codes specify the target attribute struct
// that current result will be bound to.
// The "uid" in the example codes is the table field name of the result, and the "Uid" is the relational
// struct attribute name. It automatically calculates the HasOne/HasMany relationship with given <relation>
// parameter.
// See the example or unit testing cases for clear understanding for this function.
func (m *Model) ScanList(listPointer interface{}, attributeName string, relation ...string) (err error)

该方法用于将查询到的数组数据绑定到指定的列表上,例如:

  • ScanList(&users, "User")

表示将查询到的用户信息数组数据绑定到users列表中每一项的User属性上。

  • ScanList(&users, "UserDetail", "User", "uid:Uid")

表示将查询到用户详情数组数据绑定到users列表中每一项的UserDetail属性上,并且和另一个User对象属性通过uid:Uid字段:属性关联,内部将会根据这一关联关系自动进行数据绑定。其中uid:Uid前面的uid表示查询结果字段中的uid字段,后面的Uid表示目标关联对象中的Uid属性。

  • ScanList(&users, "UserScores", "User", "uid:Uid")

表示将查询到用户详情数组数据绑定到users列表中每一项的UserScores属性上,并且和另一个User对象属性通过uid:Uid字段:属性关联,内部将会根据这一关联关系自动进行数据绑定。由于UserScores是一个数组类型[]*EntityUserScores,因此该方法内部可以自动识别到UserUserScores其实是1:N的关系,自动完成数据绑定。

需要提醒的是,如果关联数据中对应的关联属性数据不存在,那么该属性不会被初始化并将保持nil

2. ListItemValues/ListItemValuesUnique

方法定义:

// ListItemValues retrieves and returns the elements of all item struct/map with key <key>.
// Note that the parameter <list> should be type of slice which contains elements of map or struct,
// or else it returns an empty slice.
//
// The parameter <list> supports types like:
// []map[string]interface{}
// []map[string]sub-map
// []struct
// []struct:sub-struct
// Note that the sub-map/sub-struct makes sense only if the optional parameter <subKey> is given.
func ListItemValues(list interface{}, key interface{}, subKey ...interface{}) (values []interface{})  

// ListItemValuesUnique retrieves and returns the unique elements of all struct/map with key <key>.
// Note that the parameter <list> should be type of slice which contains elements of map or struct,
// or else it returns an empty slice.
// See gutil.ListItemValuesUnique.
func ListItemValuesUnique(list interface{}, key string, subKey ...interface{}) []interface{}
ListItemValuesUniqueListItemValues方法的区别在于过滤重复的返回值,保证返回的列表数据中不带有重复值。这两个方法都会在当给定的列表中包含struct/map数据项时,用于获取指定属性/键名的数据值,构造成数组[]interface{}返回。示例:
  • gdb.ListItemValuesUnique(users, "Uid")用于获取users数组中,每一个Uid属性,构造成[]interface{}数组返回。这里以便根据uid构造成SELECT...IN...查询。
  • gdb.ListItemValuesUnique(users, "User", "Uid")用于获取users数组中,每一个User属性项中的Uid属性,构造成[]interface{}数组返回。这里以便根据uid构造成SELECT...IN...查询。




Content Menu

  • No labels

38 Comments

  1. 当正向关联后再反向关联就会报import cycle not allowed

    这块应该怎么处理

    刘洋 你应该需要的是dao设计,请参考下相关设计章节:对象封装设计(更新中)

  2. 这个1对多的正相关联关系不能定义反向关联吗,类似于PHP的belongsTo这种定义

    1. 其实这里不存在模型间的关联关系的提前定义了,关联关系由业务逻辑查询的时候通过Scan/ScanList方法自行绑定关联数据模型,按照数据对象设计时候的关系自行绑定即可。gdb组件在这里做的是提供便捷方法组装数据,并没有帮开发者维护模型关联信息。

      1. Scan/ScanList这个我知道,我现在的问题是我定义这种结构体应该怎么定义呢
        就是1:N的反向定义, 我现在查询副表,然后关联主表信息
        前提是我1:N的正向关系已经定义了

        1. 你来个问题的例子,我可以写给你看。

          1. 比如通过查询EntityUserScores信息,然后需要把EntityUser的信息也关联查出来

            1. package main
              
              import (
              	"github.com/gogf/gf/database/gdb"
              	"github.com/gogf/gf/frame/g"
              )
              
              // 用户表
              type EntityUser struct {
              	Uid  int    `orm:"uid"`
              	Name string `orm:"name"`
              }
              
              // 用户学分
              type EntityUserScore struct {
              	Id     int    `orm:"id"`
              	Uid    int    `orm:"uid"`
              	Score  int    `orm:"score"`
              	Course string `orm:"course"`
              }
              
              // 用户学分+用户信息
              type Entity struct {
              	User  *EntityUser
              	Score *EntityUserScore
              }
              
              func main() {
              	var entities []*Entity
              	g.DB().SetDebug(true)
              	// 查询用户学分数据
              	// SELECT * FROM `user_scores`
              	err := g.DB().Table("user_scores").ScanList(&entities, "Score")
              	if err != nil {
              		panic(err)
              	}
              	// 查询用户基础数据
              	// SELECT * FROM `user` WHERE `uid` IN(1)
              	err = g.DB().Table("user").
              		Where("uid", gdb.ListItemValuesUnique(entities, "Score", "Uid")).
              		ScanList(&entities, "User", "Score", "uid:Uid")
              	if err != nil {
              		panic(err)
              	}
              	g.Dump(entities)
              }
              1. 不是这个意思  我的意思是前提已经有这个了

                // 组合模型,用户信息
                type Entity struct {
                    User       *EntityUser
                    UserDetail *EntityUserDetail
                    UserScores []*EntityUserScores
                }

                然后怎么定义User和UserScores之间的关系

                上面那个Entity里的User和UserScores之间的关系是1:N,我需要的UserScores和User之间的是1:1的关系

                1. 哥,那你需要的不就是UserUserDetail的关系吗,不够参考吗?

                  1. 示例中定义的struct是放在user_model里的,userdetail之间的关系是1:N

                    我现在需要的是在detail_model里定义struct,是detail关联user,关系是1:1

                    这样模型就会重复引用,会报 import cycle not allowed

                    1. 你可能是使用的旧版的model方式,那种方式如果包依赖设计不好就有这样的问题,所以才会有dao这种方式,请参考:对象封装设计(更新中)

  3. 总觉得这个关联模型有点别扭,不如BelongsTo, HasOne, HasMany, ManyToMany这样的模型关联设计用起来舒服,对于我来说,我宁愿自己维护一下模型struct中的标签备注,这样维护一次,以后每次调用的时候,至少不用我手动查两次库,然后再用scan去绑定关系了


    1. 你可以来个例子,我们仔细交流下。

      1. 没想好怎么实现,不过我知道我想要什么亚子滴,大致是这样吧

        // 用户表
        type EntityUser struct {
            Uid  int    `orm:"uid"`
            Name string `orm:"name"`
            Detail *EntityUserDetail `with:"uid=user_id"`
        }
        // 用户详情
        type EntityUserDetail struct {
            UserId     int    `orm:"user_id"`
            Address string `orm:"address"`
        }
        dao.User.With(&EntityUserDetail).Where("uid=?",1)
        // With方法会执行两条sql
        // 查询用户基础数据
        // SELECT uid,name FROM `user` WHERE `uid`=1
        // SELECT user_id,address FROM `user_detail` WHERE `user_id`=1

        就是has One吧


        1. 这个方法不错,  我也觉得可行..

        2. 好的,我明白了,现在的ScanList可能还比较偏底层一些,易用性可以再改进一下。感谢你的建议,我想了想可以进一步这么改进:

          1. 现在的model增加TableName方法,用于识别该model对象涉及到的表明,以便后续与其他表关联查询时不再需要开发者输入数据表名。这个也可以通过cli工具全自动生成。
          2. 结构体属性按照你这样设计不错,不过数据库相关的标签用orm标签统一管理比较好一些。

          最终的效果可能如下:

          package main
          
          import (
          	"focus/app/dao"
          	"github.com/gogf/gf/frame/g"
          )
          
          // 用户表
          type User struct {
          	Uid    int         `orm:"uid"`
          	Name   string      `orm:"name"`
          	Detail *UserDetail `orm:"ref:user_id=uid"`
          }
          
          // 用户详情
          type UserDetail struct {
          	UserId  int    `orm:"user_id"`
          	Address string `orm:"address"`
          }
          
          func (m *User) TableName() string {
          	return "user"
          }
          
          func (m *UserDetail) TableName() string {
          	return "user_detail"
          }
          
          func main() {
          	dao.User.Where("uid", 1)
          	// 查询会自动执行两条SQL:
          	// SELECT `uid`,`name` FROM `user` WHERE `uid`=1
          	// SELECT `user_id`,`address` FROM `user_detail` WHERE `user_id`=1
          
          	dao.User.Where("uid", g.Slice{1, 2, 3})
          	// 查询会自动执行两条SQL:
          	// SELECT `uid`,`name` FROM `user` WHERE `uid` IN(1,2,3)
          	// SELECT `user_id`,`address` FROM `user_detail` WHERE `user_id` IN(1,2,3)
          }
          
          

          诸君意下如何?

          1. 这样挺好的,不过,有一个小问题!

            就是需要的时候才执行第二条sql语句,不能让struct中有orm:"ref:user_id=uid"这个标签的就自动执行!

            就是需要一个特定的方法来自行!懂我的意思吧!比如下面的With这样

            dao.User.With(&EntityUserDetail).Where("uid=?",1)



            1. 嗯,好建议,我想想哈。

              1. 嗯嗯,还要想想办法,能自定义第二条sql的Fields


  4. `    if err := dao.Page(req.Page-1, req.Limit).Where(dao.genCond(req)).
            ScanList(&list, "Record").Error(); err != "" {
            g.Log().Ctx(ctx).Warning("query records failed:", err, req)
            return nil, 500
    `
    goframe will throw panic when err is nil.
    runtime error: invalid memory address or nil pointer dereference

    github上不去了,这里提一下吧。
    goframe 1.15.3 golang 1.15.6

    补充一下:之前orm用gorm,所以会顺手写上scan().error。

    • ScanList(&users, "UserDetail", "User", "uid:Uid")

    表示将查询到用户详情数组数据绑定到users列表中每一项的UserDetail属性上,并且和另一个User对象属性通过uid:Uid字段:属性关联,内部将会根据这一关联关系自动进行数据绑定。其中uid:Uid前面的uid表示查询结果字段中的uid字段,后面的Uid表示目标关联对象中的Uid属性。


    刚刚尝试了很久,终于成功了。觉得文档这样改下更好懂:
    表示将查询到的用户详情数组绑定到users列表中对应的UserDetail属性上,绑定时,与users中的User对象通过uid:Uid字段:属性关联,其中uid:Uid前面的uid表示查询结果字段中的uid字段(user_scores表中的字段名),后面的Uid表示目标关联对象(users.User)中的Uid属性(结构体变量名)。内部会根据对应的关系进行自动数据绑定。

  5. var list []model.RecordAndMedicationList
    if err := dao.DB.Table("comm_medical_record").Page((req.Page-1)*req.Limit, req.Limit).Where(dao.genCond(req)).Order("create_time desc").
    	ScanList(&list, "Record"); err != nil {
    	g.Log().Ctx(ctx).Warning("查询病例记录失败:", err, req)
    	return nil, 500
    }
    if err := dao.DB.Table("comm_medications").
    	Where("Medic_Record_Id", gdb.ListItemValuesUnique(list, "Record", "RecordId")).
    	ScanList(&list, "Medications", "Record", "medic_record_id:RecordId"); err != nil {
    	g.Log().Ctx(ctx).Warning("查询病例记录关联的用药记录失败:", err, req)
    	// 疑似gf orm存在问题,当第一次查询只有单条记录,第二次查询没有得到数据,数据关联时,会返回Error sql: no rows in result set
    
    	// 希望返回nil,仅仅由于字段不匹配、数据库连接错误等问题再返回error
    	// return nil, 500 暂时注释掉
    }
    return &list, 200
  6. 我研究了下,关联模型需要用到非匿名字段,这样做最后处理的结果转成json都包含了该字段名.我想实现匿名字段那种json的效果有办法么.

    例如:

    type SysUserPageEntityRes struct {
        User *SysUserPageRes
        SysEmo *SysEmoInfores
    }

    json结果 都包含在了 usersysemo两个子级中了,而我使用匿名变量json结果直接在顶级中。

    1. 可以使用embedded方式嵌入结构体。

      1. ScanList(&users, "UserScores", "User", "uid:Uid")

        embedded方式嵌入,第二个参数 UserScores 应该咋填,因为没有名字啊


        1. 其实是有名字的,就是embedded的结构体名字。

          1. 原来如此 谢谢

  7. 请问在scanList不能使用以下的结构进行模型关联吗,在使用with之后发现二者风格不太统一

     // 用户详情
    type EntityUserDetail struct {
        Uid     int    `orm:"uid"`
        Address string `orm:"address"`
    }
    // 用户学分
    type EntityUserScores struct {
        Id     int    `orm:"id"`
        Uid    int    `orm:"uid"`
        Score  int    `orm:"score"`
        Course string `orm:"course"`
    }
    // 组合模型,用户信息
    type Entity struct {
        Uid  int    `orm:"uid"`
        Name string `orm:"name"`
       UserDetail *EntityUserDetail UserScores []*EntityUserScores }
    1. 可以的,如果不行的话可以先自己研究下,不行就把代码提issue。

  8. 这点我有点疑问,现在热门ORM都支持Hasone BelongsTo这种关联方式,大多数开发者也已经习惯以此方式进行关联,不支持此关联方式是否与大环境背道而驰,反而会造成开发者的心智负担呢,我个人还是比较喜欢Hasone BelongsTo这种关联方式的

    1. 这里算是一种尝试,其中 aries 建议中的with特性挺好的,目前也是在尝试阶段。

  9. 请问这种使用多次select的查询方法,是不是不能做到子表的字段进行排序,例如EntityUserScores的score字段

    1. 期待大佬解答, 我也遇到这个问题

  10. 你好,我想请问一下,多对多的关联查询应该如何实现呢?

    举例说明一下:

    // 用户表
    type User struct {
    	Id uint
    	Name string
    	Identities []Identity
    }
    
    // 用户 - 身份关联表
    type UserIdentity struct {
    	UserId uint
    	IdentityId uint
    }
    
    // 身份表
    type Identity struct {
    	Id uint
    	Name string
    }
    
    // 需要查询的结果
    var resp []User

    如上所示,如果我需要查询用户的列表,并且用户的需要关联查询身份信息应该如何查询?

  11. v2 发布后 还是实验特性吗

  12. 感觉多对多这块不能省, 希望可以像with一样 通过 g.meta 去实现.

    不然业务方面也是得开发者自己实现,多写好多代码.

    1. 赞同,建议还是跟着主流办法走,大多数开发者都熟悉主流框架的关联模式的

  13. 我在2.1版本测出这样一个问题: 

    model/user.go

    type User struct {
    Uid int `orm:"uid"`
    Name string `orm:"name"`
    }

    type UserDetail struct {
    Uid int `orm:"uid"`
    Address string `orm:"address"`
    }

    type UserScores struct {
    Id int `orm:"id"`
    Uid int `orm:"uid"`
    Score int `orm:"score"`
    Course string `orm:"course"`
    }

    // 组合模型,用户信息
    type UserItem struct {
    User *User
    UserDetail *UserDetail
    UserScores []*UserScores
    }

    type UserListInput struct {
    Uid int
    Name string
    }

    logic/user/user.go
    func (s *sUser) GetList(ctx context.Context, in model.UserListInput) (count int, list []model.UserItem, err error) {
    model := dao.User.Ctx(ctx)
    if in.Name != "" {
    model = model.WhereLike("name", "%"+in.Name+"%")
    }
    count, err = model.Count()
    err = model.ScanList(&list, "User")
    err = model.ScanList(&list, "UserDetail", "User", "uid:Uid")
    err = model.ScanList(&list, "UserScores", "User", "uid:Uid")
    g.Dump(list)
    return
    }


    然而得到的结果关联的数据只有UID映射了, 其他的数据都为空, 在2.0是正常的

    2022-06-25 16:59:41.449 [DEBU] {2cae1a5e73d1fb1696e8230ecc37f6fa} [ 30 ms] [default] [rows:3  ] SELECT `uid`,`name` FROM `user` 
    2022-06-25 16:59:41.449 [DEBU] {2cae1a5e73d1fb1696e8230ecc37f6fa} [  0 ms] [default] [rows:3  ] SELECT `uid` FROM `user` 
    2022-06-25 16:59:41.451 [DEBU] {2cae1a5e73d1fb1696e8230ecc37f6fa} [  1 ms] [default] [rows:3  ] SELECT `uid` FROM `user` 
    [
        {
            User:       {
                Uid:  1,
                Name: "jack",
            },
            UserDetail: {
                Uid:     1,
                Address: "",
            },
            UserScores: [
                {
                    Id:     0,
                    Uid:    1,
                    Score:  0,
                    Course: "",
                },
            ],
        },
        {
            User:       {
                Uid:  2,
                Name: "lucy",
            },
            UserDetail: {
                Uid:     2,
                Address: "",
            },
            UserScores: [
                {
                    Id:     0,
                    Uid:    2,
                    Score:  0,
                    Course: "",
                },
            ],
        },
        {
            User:       {
                Uid:  3,
                Name: "smith",
            },
            UserDetail: {
                Uid:     3,
                Address: "",
            },
            UserScores: [
                {
                    Id:     0,
                    Uid:    3,
                    Score:  0,
                    Course: "",
                },
            ],
        },
    ]

    而且UserScores 应该是个列表啊