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

模型关联-动态关联-ScanList - 图1注意

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

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

数据结构

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

  1. # 用户表
  2. CREATE TABLE `user` (
  3. uid int(10) unsigned NOT NULL AUTO_INCREMENT,
  4. name varchar(45) NOT NULL,
  5. PRIMARY KEY (uid)
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  7. # 用户详情
  8. CREATE TABLE `user_detail` (
  9. uid int(10) unsigned NOT NULL AUTO_INCREMENT,
  10. address varchar(45) NOT NULL,
  11. PRIMARY KEY (uid)
  12. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  13. # 用户学分
  14. CREATE TABLE `user_scores` (
  15. id int(10) unsigned NOT NULL AUTO_INCREMENT,
  16. uid int(10) unsigned NOT NULL,
  17. score int(10) unsigned NOT NULL,
  18. course varchar(45) NOT NULL,
  19. PRIMARY KEY (id)
  20. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

数据模型

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

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

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

  1. // 用户表
  2. type EntityUser struct {
  3. Uid int `orm:"uid"`
  4. Name string `orm:"name"`
  5. }
  6. // 用户详情
  7. type EntityUserDetail struct {
  8. Uid int `orm:"uid"`
  9. Address string `orm:"address"`
  10. }
  11. // 用户学分
  12. type EntityUserScores struct {
  13. Id int `orm:"id"`
  14. Uid int `orm:"uid"`
  15. Score int `orm:"score"`
  16. Course string `orm:"course"`
  17. }
  18. // 组合模型,用户信息
  19. type Entity struct {
  20. User *EntityUser
  21. UserDetail *EntityUserDetail
  22. UserScores []*EntityUserScores
  23. }

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

数据写入

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

  1. err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
  2. r, err := tx.Model("user").Save(EntityUser{
  3. Name: "john",
  4. })
  5. if err != nil {
  6. return err
  7. }
  8. uid, err := r.LastInsertId()
  9. if err != nil {
  10. return err
  11. }
  12. _, err = tx.Model("user_detail").Save(EntityUserDetail{
  13. Uid: int(uid),
  14. Address: "Beijing DongZhiMen #66",
  15. })
  16. if err != nil {
  17. return err
  18. }
  19. _, err = tx.Model("user_scores").Save(g.Slice{
  20. EntityUserScores{Uid: int(uid), Score: 100, Course: "math"},
  21. EntityUserScores{Uid: int(uid), Score: 99, Course: "physics"},
  22. })
  23. return err
  24. })

数据查询

单条数据记录

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

  1. // 定义用户列表
  2. var user Entity
  3. // 查询用户基础数据
  4. // SELECT * FROM `user` WHERE `name`='john'
  5. err := g.Model("user").Scan(&user.User, "name", "john")
  6. if err != nil {
  7. return err
  8. }
  9. // 查询用户详情数据
  10. // SELECT * FROM `user_detail` WHERE `uid`=1
  11. err := g.Model("user_detail").Scan(&user.UserDetail, "uid", user.User.Uid)
  12. // 查询用户学分数据
  13. // SELECT * FROM `user_scores` WHERE `uid`=1
  14. err := g.Model("user_scores").Scan(&user.UserScores, "uid", user.User.Uid)

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

多条数据记录

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

  1. // 定义用户列表
  2. var users []Entity
  3. // 查询用户基础数据
  4. // SELECT * FROM `user`
  5. err := g.Model("user").ScanList(&users, "User")
  6. // 查询用户详情数据
  7. // SELECT * FROM `user_detail` WHERE `uid` IN(1,2)
  8. err := g.Model("user_detail").
  9. Where("uid", gdb.ListItemValuesUnique(users, "User", "Uid")).
  10. ScanList(&users, "UserDetail", "User", "uid:Uid")
  11. // 查询用户学分数据
  12. // SELECT * FROM `user_scores` WHERE `uid` IN(1,2)
  13. err := g.Model("user_scores").
  14. Where("uid", gdb.ListItemValuesUnique(users, "User", "Uid")).
  15. ScanList(&users, "UserScores", "User", "uid:Uid")

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

1. ScanList

方法定义:

  1. // ScanList converts <r> to struct slice which contains other complex struct attributes.
  2. // Note that the parameter <listPointer> should be type of *[]struct/*[]*struct.
  3. // Usage example:
  4. //
  5. // type Entity struct {
  6. // User *EntityUser
  7. // UserDetail *EntityUserDetail
  8. // UserScores []*EntityUserScores
  9. // }
  10. // var users []*Entity
  11. // or
  12. // var users []Entity
  13. //
  14. // ScanList(&users, "User")
  15. // ScanList(&users, "UserDetail", "User", "uid:Uid")
  16. // ScanList(&users, "UserScores", "User", "uid:Uid")
  17. // The parameters "User"/"UserDetail"/"UserScores" in the example codes specify the target attribute struct
  18. // that current result will be bound to.
  19. // The "uid" in the example codes is the table field name of the result, and the "Uid" is the relational
  20. // struct attribute name. It automatically calculates the HasOne/HasMany relationship with given <relation>
  21. // parameter.
  22. // See the example or unit testing cases for clear understanding for this function.
  23. 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

方法定义:

  1. // ListItemValues retrieves and returns the elements of all item struct/map with key <key>.
  2. // Note that the parameter <list> should be type of slice which contains elements of map or struct,
  3. // or else it returns an empty slice.
  4. //
  5. // The parameter <list> supports types like:
  6. // []map[string]interface{}
  7. // []map[string]sub-map
  8. // []struct
  9. // []struct:sub-struct
  10. // Note that the sub-map/sub-struct makes sense only if the optional parameter <subKey> is given.
  11. func ListItemValues(list interface{}, key interface{}, subKey ...interface{}) (values []interface{})
  12. // ListItemValuesUnique retrieves and returns the unique elements of all struct/map with key <key>.
  13. // Note that the parameter <list> should be type of slice which contains elements of map or struct,
  14. // or else it returns an empty slice.
  15. // See gutil.ListItemValuesUnique.
  16. 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... 查询。