- Created by 郭强, last modified by 张金富 on Jun 15, 2023
gf
的ORM
没有采用其他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:N
关系。 - 这里并没有演示
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 := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { r, err := tx.Model("user").Save(EntityUser{ Name: "john", }) if err != nil { return err } uid, err := r.LastInsertId() if err != nil { return err } _, err = tx.Model("user_detail").Save(EntityUserDetail{ Uid: int(uid), Address: "Beijing DongZhiMen #66", }) if err != nil { return err } _, err = tx.Model("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 := g.Model("user").Scan(&user.User, "name", "john") if err != nil { return err } // 查询用户详情数据 // SELECT * FROM `user_detail` WHERE `uid`=1 err := g.Model("user_detail").Scan(&user.UserDetail, "uid", user.User.Uid) // 查询用户学分数据 // SELECT * FROM `user_scores` WHERE `uid`=1 err := g.Model("user_scores").Scan(&user.UserScores, "uid", user.User.Uid)
该方法在之前的章节中已经有介绍,因此这里不再赘述。
多条数据记录
查询多条数据记录并绑定数据到数据模型数组中,需要使用到ScanList
方法,该方法会需要用户指定结果字段与模型属性的关系,随后底层会遍历数组并自动执行数据绑定。例如:
// 定义用户列表 var users []Entity // 查询用户基础数据 // SELECT * FROM `user` err := g.Model("user").ScanList(&users, "User") // 查询用户详情数据 // SELECT * FROM `user_detail` WHERE `uid` IN(1,2) err := g.Model("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 := g.Model("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
,因此该方法内部可以自动识别到User
到UserScores
其实是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{}
ListItemValuesUnique
与 ListItemValues
方法的区别在于过滤重复的返回值,保证返回的列表数据中不带有重复值。这两个方法都会在 当给定的列表中包含 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...
查询。
- No labels
42 Comments
郭强
刘洋 你应该需要的是
dao
设计,请参考下相关设计章节:对象封装设计(更新中)CasperChan
这个对象封装设计怎么一直是禁止打开的状态
刘洋
这个1对多的正相关联关系不能定义反向关联吗,类似于PHP的belongsTo这种定义
郭强
其实这里不存在模型间的关联关系的提前定义了,关联关系由业务逻辑查询的时候通过
Scan/ScanList
方法自行绑定关联数据模型,按照数据对象设计时候的关系自行绑定即可。gdb
组件在这里做的是提供便捷方法组装数据,并没有帮开发者维护模型关联信息。刘洋
Scan/ScanList这个我知道,我现在的问题是我定义这种结构体应该怎么定义呢
就是1:N的反向定义, 我现在查询副表,然后关联主表信息
前提是我1:N的正向关系已经定义了
郭强
你来个问题的例子,我可以写给你看。
刘洋
比如通过查询EntityUserScores信息,然后需要把EntityUser的信息也关联查出来
郭强
刘洋
不是这个意思 我的意思是前提已经有这个了
然后怎么定义User和UserScores之间的关系
上面那个Entity里的User和UserScores之间的关系是1:N,我需要的UserScores和User之间的是1:1的关系
郭强
哥,那你需要的不就是
User
和UserDetail
的关系吗,不够参考吗?刘洋
示例中定义的
struct
是放在user_model
里的,user
和detail
之间的关系是1:N
我现在需要的是在
detail_model
里定义struct
,是detail
关联user
,关系是1:1
,这样模型就会重复引用,会报
import cycle not allowed
郭强
你可能是使用的旧版的
model
方式,那种方式如果包依赖设计不好就有这样的问题,所以才会有dao
这种方式,请参考:对象封装设计(更新中)aries
总觉得这个关联模型有点别扭,不如
BelongsTo
,HasOne
,HasMany
,ManyToMany
这样的模型关联设计用起来舒服,对于我来说,我宁愿自己维护一下模型struct中的标签备注,这样维护一次,以后每次调用的时候,至少不用我手动查两次库,然后再用scan去绑定关系了郭强
你可以来个例子,我们仔细交流下。
aries
没想好怎么实现,不过我知道我想要什么亚子滴,大致是这样吧
就是has One吧
Xsoul
这个方法不错, 我也觉得可行..
郭强
好的,我明白了,现在的
ScanList
可能还比较偏底层一些,易用性可以再改进一下。感谢你的建议,我想了想可以进一步这么改进:model
增加TableName
方法,用于识别该model
对象涉及到的表明,以便后续与其他表关联查询时不再需要开发者输入数据表名。这个也可以通过cli
工具全自动生成。orm
标签统一管理比较好一些。最终的效果可能如下:
诸君意下如何?
aries
这样挺好的,不过,有一个小问题!
就是需要的时候才执行第二条
sql
语句,不能让struct
中有orm:"ref:user_id=uid"
这个标签的就自动执行!就是需要一个特定的方法来自行!懂我的意思吧!比如下面的With这样
郭强
嗯,好建议,我想想哈。
aries
嗯嗯,还要想想办法,能自定义第二条sql的Fields
浪里寻花
` 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
属性(结构体变量名)。内部会根据对应的关系进行自动数据绑定。浪里寻花
xushushun
我研究了下,关联模型需要用到非匿名字段,这样做最后处理的结果转成json都包含了该字段名.我想实现匿名字段那种json的效果有办法么.
例如:
json
结果 都包含在了user
和sysemo
两个子级中了,而我使用匿名变量json
结果直接在顶级中。郭强
可以使用
embedded
方式嵌入结构体。xushushun
embedded
方式嵌入,第二个参数UserScores
应该咋填,因为没有名字啊郭强
其实是有名字的,就是
embedded
的结构体名字。xushushun
原来如此 谢谢
陈富贵
请问在scanList不能使用以下的结构进行模型关联吗,在使用with之后发现二者风格不太统一
郭强
可以的,如果不行的话可以先自己研究下,不行就把代码提issue。
ysxpark
这点我有点疑问,现在热门ORM都支持Hasone BelongsTo这种关联方式,大多数开发者也已经习惯以此方式进行关联,不支持此关联方式是否与大环境背道而驰,反而会造成开发者的心智负担呢,我个人还是比较喜欢Hasone BelongsTo这种关联方式的
郭强
这里算是一种尝试,其中 aries 建议中的
with
特性挺好的,目前也是在尝试阶段。陈富贵
请问这种使用多次select的查询方法,是不是不能做到子表的字段进行排序,例如EntityUserScores的score字段
jangbx
期待大佬解答, 我也遇到这个问题
王一飞
你好,我想请问一下,多对多的关联查询应该如何实现呢?
举例说明一下:
如上所示,如果我需要查询用户的列表,并且用户的需要关联查询身份信息应该如何查询?
goer
v2 发布后 还是实验特性吗
Victor
感觉多对多这块不能省, 希望可以像with一样 通过 g.meta 去实现.
不然业务方面也是得开发者自己实现,多写好多代码.
ysxpark
赞同,建议还是跟着主流办法走,大多数开发者都熟悉主流框架的关联模式的
wilson
我在2.1版本测出这样一个问题:
model/user.go
然而得到的结果关联的数据只有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 应该是个列表啊
yiyang
有个问题,如果 UserScores里面不与user关联,而是与userDetail关联的。多UserScores对一userDetail。那怎么实现:user->userdetail→userscore 这种关联查询呢?
小陈
大佬们, 这个和LeftJoin哪个性能高点
小陈
强哥在其他帖子回复ScanList在有索引情况会更高点.