- Created by 郭强, last modified by 张金富 on Jun 15, 2023
You are viewing an old version of this page. View the current version.
Compare with Current View Page History
Version 26 Next »
一、设计背景
大家都知道易用性和易维护性一直是goframe
一直努力建设的,也是goframe
有别其他框架和组件比较大的一点差异。goframe
没有采用其他 ORM
常见的 BelongsTo
, HasOne
, HasMany
, ManyToMany
这样的模型关联设计,这样的关联关系维护较繁琐,例如外键约束、额外的标签备注等,对开发者有一定的心智负担。因此 框架不倾向于通过向模型结构体中注入过多复杂的标签内容、关联属性或方法,并一如既往地尝试着简化设计,目标是使得模型关联查询尽可能得易于理解、使用便捷。因此在之前推出了ScanList
方案,建议大家在继续了解With
特性之前先了解一下 模型关联-动态关联-ScanList 。
经过一系列的项目实践,我们发现ScanList
虽然从运行时业务逻辑的角度来维护了模型关联关系,但是这种关联关系维护也不如期望的简便。因此,我们继续改进推出了可以通过模型简单维护关联关系的With
模型关联特性,当然,这种特性仍然致力于提升整体框架的易用性和维护性,可以把With
特性看做ScanList
与模型关联关系维护的一种结合和改进。
本特性需要感谢 aries 提供的宝贵建议。
With
特性从goframe v1.15.7
版本开始提供,目前属于实验性特性。
二、举个例子
我们先来一个简单的示例,便于大家更好理解With
特性,该示例来自于之前的ScanList
章节的相同示例,改进版。
1、数据结构
# 用户表 CREATE TABLE `user` ( id int(10) unsigned NOT NULL AUTO_INCREMENT, name varchar(45) NOT NULL, PRIMARY KEY (id) ) 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, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2、数据结构
根据表定义,我们可以得知:
- 用户表与用户详情是
1:1
关系。 - 用户表与用户学分是
1:N
关系。 - 这里并没有演示
N:N
的关系,因为相比较于1:N
的查询只是多了一次关联、或者一次查询,最终处理方式和1:N
类似。
那么Golang
的模型可定义如下:
// 用户详情 type UserDetail struct { gmeta.Meta `orm:"table:user_detail"` Uid int `json:"uid"` Address string `json:"address"` } // 用户学分 type UserScores struct { gmeta.Meta `orm:"table:user_scores"` Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } // 用户信息 type User struct { gmeta.Meta `orm:"table:user"` Id int `json:"id"` Name string `json:"name"` UserDetail *UserDetail `orm:"with:uid=id"` UserScores []*UserScores `orm:"with:uid=id"` }
3、数据写入
为简化示例,我们这里创建5
条用户数据,采用事务操作方式写入:
- 用户信息,
id
为1-5
,name
为name_1
到name_5
。 - 同时创建
5
条用户详情数据,address
数据为address_1
到address_5
。 - 每个用户创建
5
条学分信息,学分为1-5
。
g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { for i := 1; i <= 5; i++ { // User. user := User{ Name: fmt.Sprintf(`name_%d`, i), } lastInsertId, err := g.Model(user).Data(user).OmitEmpty().InsertAndGetId() if err != nil { return err } // Detail. userDetail := UserDetail{ Uid: int(lastInsertId), Address: fmt.Sprintf(`address_%d`, lastInsertId), } _, err = g.Model(userDetail).Data(userDetail).OmitEmpty().Insert() if err != nil { return err } // Scores. for j := 1; j <= 5; j++ { userScore := UserScores{ Uid: int(lastInsertId), Score: j, } _, err = g.Model(userScore).Data(userScore).OmitEmpty().Insert() if err != nil { return err } } } return nil })
执行成功后,数据库数据如下:
mysql> show tables; +----------------+ | Tables_in_test | +----------------+ | user | | user_detail | | user_score | +----------------+ 3 rows in set (0.01 sec) mysql> select * from `user`; +----+--------+ | id | name | +----+--------+ | 1 | name_1 | | 2 | name_2 | | 3 | name_3 | | 4 | name_4 | | 5 | name_5 | +----+--------+ 5 rows in set (0.01 sec) mysql> select * from `user_detail`; +-----+-----------+ | uid | address | +-----+-----------+ | 1 | address_1 | | 2 | address_2 | | 3 | address_3 | | 4 | address_4 | | 5 | address_5 | +-----+-----------+ 5 rows in set (0.00 sec) mysql> select * from `user_score`; +----+-----+-------+ | id | uid | score | +----+-----+-------+ | 1 | 1 | 1 | | 2 | 1 | 2 | | 3 | 1 | 3 | | 4 | 1 | 4 | | 5 | 1 | 5 | | 6 | 2 | 1 | | 7 | 2 | 2 | | 8 | 2 | 3 | | 9 | 2 | 4 | | 10 | 2 | 5 | | 11 | 3 | 1 | | 12 | 3 | 2 | | 13 | 3 | 3 | | 14 | 3 | 4 | | 15 | 3 | 5 | | 16 | 4 | 1 | | 17 | 4 | 2 | | 18 | 4 | 3 | | 19 | 4 | 4 | | 20 | 4 | 5 | | 21 | 5 | 1 | | 22 | 5 | 2 | | 23 | 5 | 3 | | 24 | 5 | 4 | | 25 | 5 | 5 | +----+-----+-------+ 25 rows in set (0.00 sec)
4、数据查询
新的With
特性下,数据查询相当简便,例如,我们查询一条数据:
var user *User g.Model(tableUser).WithAll().Where("id", 3).Scan(&user)
以上语句您将会查询到用户ID为3
的用户信息、用户详情以及用户学分信息,以上语句将会在数据库中自动执行以下SQL
语句:
2021-05-02 22:29:52.634 [DEBU] [ 2 ms] [default] SHOW FULL COLUMNS FROM `user` 2021-05-02 22:29:52.635 [DEBU] [ 1 ms] [default] SELECT * FROM `user` WHERE `id`=3 LIMIT 1 2021-05-02 22:29:52.636 [DEBU] [ 1 ms] [default] SHOW FULL COLUMNS FROM `user_detail` 2021-05-02 22:29:52.637 [DEBU] [ 1 ms] [default] SELECT `uid`,`address` FROM `user_detail` WHERE `uid`=3 LIMIT 1 2021-05-02 22:29:52.643 [DEBU] [ 6 ms] [default] SHOW FULL COLUMNS FROM `user_score` 2021-05-02 22:29:52.644 [DEBU] [ 0 ms] [default] SELECT `id`,`uid`,`score` FROM `user_score` WHERE `uid`=3
执行后,通过g.Dump(user)
打印的用户信息如下:
{ Id: 3, Name: "name_3", UserDetail: { Uid: 3, Address: "address_3", }, UserScores: [ { Id: 11, Uid: 3, Score: 1, }, { Id: 12, Uid: 3, Score: 2, }, { Id: 13, Uid: 3, Score: 3, }, { Id: 14, Uid: 3, Score: 4, }, { Id: 15, Uid: 3, Score: 5, }, ], }
5、列表查询
我们来一个通过With
特性查询列表的示例:
var users []*User g.Model(users).With(UserDetail{}).Where("id>?", 3).Scan(&users)
执行后,通过g.Dump(users)
打印用户数据如下:
[ { Id: 4, Name: "name_4", UserDetail: { Uid: 4, Address: "address_4", }, UserScores: [], }, { Id: 5, Name: "name_5", UserDetail: { Uid: 5, Address: "address_5", }, UserScores: [], }, ]
6、条件与排序
通过With
特性关联时可以指定关联的额外条件,以及在多数据结果下指定排序规则。例如:
type User struct { gmeta.Meta `orm:"table:user"` Id int `json:"id"` Name string `json:"name"` UserDetail *UserDetail `orm:"with:uid=id, where:uid > 3"` UserScores []*UserScores `orm:"with:uid=id, where:score>1 and score<5, order:score desc"` }
通过orm
标签中的where
子标签以及order
子标签指定额外关联条件体积排序规则。
三、详细说明
想必您一定对上面的某些使用比较好奇,比如gmeta
包、比如WithAll
方法、比如orm
标签中的with
语句、比如Model
方法给定struct
参数识别数据表名等等,那这就对啦,接下来,我们详细聊聊吧。
1、gmeta
包
我们可以看到在上面的结构体数据结构中都使用embed
方式嵌入了一个gmeta.Meta
结构体,例如:
type UserDetail struct { gmeta.Meta `orm:"table:user_detail"` Uid int `json:"uid"` Address string `json:"address"` }
其实在GoFrame
框架中有很多这种小组件包用以实现特定的便捷功能。gmeta
包的作用主要用于嵌入到用户自定义的结构体中,并且通过标签的形式给gmeta
包的结构体(例如这里的gmeta.Meta
)打上自定义的标签内容(列如这里的`orm:"table:user_detail"`
),并在运行时可以特定方法动态获取这些自定义的标签内容。详情请参考章节:元数据-gmeta
因此,这里嵌入gmeta.Meta
的目的是为了标记该结构体关联的数据表名称。
2、模型关联指定
在如下结构体中:
type User struct { gmeta.Meta `orm:"table:user"` Id int `json:"id"` Name string `json:"name"` UserDetail *UserDetail `orm:"with:uid=id"` UserScores []*UserScore `orm:"with:uid=id"` }
我们通过给指定的结构体属性绑定orm
标签,并在orm
标签中通过with
语句指定当前结构体(数据表)与目标结构体(数据表)的关联关系,with
语句的语法如下:
with:当前属性对应表关联字段=当前结构体对应数据表关联字段
并且字段名称忽略大小写以及特殊字符匹配,例如以下形式的关联关系都是能够自动识别的:
with:UID=ID with:Uid=Id with:U_ID=id
如果两个表的关联字段都是同一个名称,那么也可以直接写一个即可,例如:
with:uid
在本示例中,UserDetail
属性对应的数据表为user_detail
,UserScores
属性对应的数据表为user_score
,两者与当前User
结构体对应的表user
都是使用uid
进行关联,并且目标关联的user
表的对应字段为id
。
3、With/WithAll
1)基本介绍
默认情况下,即使我们的结构体属性中的orm
标签带有with
语句,ORM
组件并不会默认启用With
特性进行关联查询,而是需要依靠With/WithAll
方法启用该查询特性。
With
:指定启用关联查询的数据表,通过给定的属性对象指定。WithAll
:启用操作对象中所有带有with
语句的属性结构体关联查询。
在我们本示例中,使用的是WithAll
方法,因此自动启用了User
表中的所有属性的模型关联查询,只要属性结构体关联了数据表,并且orm
标签中带有with
语句,那么都将会自动查询数据并根据模型结构的关联关系进行数据绑定。假如我们只启用某部分关联查询,并不启用全部属性模型的关联查询,那么可以使用With
方法来指定。并且With
方法可以指定启用多个关联模型的自动查询,在本示例中的WithAll
就相当于:
var user *User g.Model(tableUser).With(UserDetail{}, UserScore{}).Where("id", 3).Scan(&user)
也可以这样:
var user *User g.Model(tableUser).With(User{}.UserDetail, User{}.UserScore).Where("id", 3).Scan(&user)
2)仅关联用户详情模型
假如我们只需要查询用户详情,并不需要查询用户学分,那么我们可以使用With
方法来启用指定对象对应数据表的关联查询,例如:
var user *User g.Model(tableUser).With(UserDetail{}).Where("id", 3).Scan(&user)
也可以这样:
var user *User g.Model(tableUser).With(User{}.UserDetail).Where("id", 3).Scan(&user)
执行后,通过g.Dump(user)
打印用户数据如下:
{ "id": 3, "name": "name_3", "UserDetail": { "uid": 3, "address": "address_3" }, "UserScores": null }
3)仅关联用户学分模型
我们也可以只关联查询用户学分信息,例如:
var user *User g.Model(tableUser).With(UserScore{}).Where("id", 3).Scan(&user)
也可以这样:
var user *User g.Model(tableUser).With(User{}.UserScore).Where("id", 3).Scan(&user)
执行后,通过g.Dump(user)
打印用户数据如下:
{ "id": 3, "name": "name_3", "UserDetail": null, "UserScores": [ { "id": 11, "uid": 3, "score": 1 }, { "id": 12, "uid": 3, "score": 2 }, { "id": 13, "uid": 3, "score": 3 }, { "id": 14, "uid": 3, "score": 4 }, { "id": 15, "uid": 3, "score": 5 } ] }
4)不关联任何模型查询
假如,我们不需要关联查询,那么更简单,例如:
var user *User g.Model(tableUser).Where("id", 3).Scan(&user)
执行后,通过g.Dump(user)
打印用户数据如下:
{ "id": 3, "name": "name_3", "UserDetail": null, "UserScores": null }
四、使用限制
1、字段查询与过滤
可以看到,在我们上面的示例中,并没有指定查询的字段,但是在打印的SQL
日志中可以看到查询语句不是简单的SELECT *
而是执行了具体的字段查询。在With
特性下,将会自动按照关联模型对象的属性进行查询,属性的名称将会与数据表的字段做自动映射,并且会自动过滤掉无法自动映射的字段查询。
所以,在With
特性下,我们无法做到仅查询属性中对应的某几个字段。如果需要实现仅查询并赋值某几个字段,建议您对model
数据结构按照业务场景进行裁剪,创建满足特定业务场景的数据结构,而不是使用一个数据结构满足不同的多个场景。
我们来一个示例更好说明。假如我们有一个实体对象数据结构Content
,一个常见的CMS
系统的内容模型如下,该模型与数据表字段一一对应:
type Content struct { Id uint `orm:"id,primary" json:"id"` // 自增ID Key string `orm:"key" json:"key"` // 唯一键名,用于程序硬编码,一般不常用 Type string `orm:"type" json:"type"` // 内容模型: topic, ask, article等,具体由程序定义 CategoryId uint `orm:"category_id" json:"category_id"` // 栏目ID UserId uint `orm:"user_id" json:"user_id"` // 用户ID Title string `orm:"title" json:"title"` // 标题 Content string `orm:"content" json:"content"` // 内容 Sort uint `orm:"sort" json:"sort"` // 排序,数值越低越靠前,默认为添加时的时间戳,可用于置顶 Brief string `orm:"brief" json:"brief"` // 摘要 Thumb string `orm:"thumb" json:"thumb"` // 缩略图 Tags string `orm:"tags" json:"tags"` // 标签名称列表,以JSON存储 Referer string `orm:"referer" json:"referer"` // 内容来源,例如github/gitee Status uint `orm:"status" json:"status"` // 状态 0: 正常, 1: 禁用 ReplyCount uint `orm:"reply_count" json:"reply_count"` // 回复数量 ViewCount uint `orm:"view_count" json:"view_count"` // 浏览数量 ZanCount uint `orm:"zan_count" json:"zan_count"` // 赞 CaiCount uint `orm:"cai_count" json:"cai_count"` // 踩 CreatedAt *gtime.Time `orm:"created_at" json:"created_at"` // 创建时间 UpdatedAt *gtime.Time `orm:"updated_at" json:"updated_at"` // 修改时间 }
内容的列表页又不需要展示这么详细的内容,特别是其中的Content
字段非常大,我们列表页只需要查询几个字段而已。那么我们可以单独定义一个用于列表的返回数据结构(字段裁剪),而不是直接使用数据表实体对象数据结构。例如:
type ContentListItem struct { Id uint `json:"id"` // 自增ID CategoryId uint `json:"category_id"` // 栏目ID UserId uint `json:"user_id"` // 用户ID Title string `json:"title"` // 标题 CreatedAt *gtime.Time `json:"created_at"` // 创建时间 UpdatedAt *gtime.Time `json:"updated_at"` // 修改时间 }
2、必须存在关联字段属性
由于With
特性是通过识别数据结构关联关系,并自动执行多条SQL查询来实现的,因此关联的字段也必须作为对象的属性便于关联字段值得自动获取。简单地讲,with
标签中的字段必须存在于关联对象的属性上。
五、递归关联
如果关联的模型属性也带有with
标签,那么将会递归执行关联查询。With
特性支持无限层级的递归关联。以下示例,仅供参考:
// 用户详情 type UserDetail struct { gmeta.Meta `orm:"table:user_detail"` Uid int `json:"uid"` Address string `json:"address"` } // 用户学分 - 必修课 type UserScoresRequired struct { gmeta.Meta `orm:"table:user_scores"` Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } // 用户学分 - 选修课 type UserScoresOptional struct { gmeta.Meta `orm:"table:user_scores"` Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } // 用户学分 type UserScores struct { gmeta.Meta `orm:"table:user_scores"` Id int `json:"id"` Uid int `json:"uid"` Required []UserScoresRequired `orm:"with:id, where:type=1"` Optional []UserScoresOptional `orm:"with:id, where:type=2"` } // 用户信息 type User struct { gmeta.Meta `orm:"table:user"` Id int `json:"id"` Name string `json:"name"` UserDetail *UserDetail `orm:"with:uid=id"` UserScores []*UserScores `orm:"with:uid=id"` }
六、模型示例
根据当前的数据表,这里给了更多的一些模型编写示例供大家参考。
1、关联模型嵌套
type UserDetail struct { gmeta.Meta `orm:"table:user_detail"` Uid int `json:"uid"` Address string `json:"address"` } type UserScores struct { gmeta.Meta `orm:"table:user_scores"` Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } type User struct { gmeta.Meta `orm:"table:user"` *UserDetail `orm:"with:uid=id"` Id int `json:"id"` Name string `json:"name"` UserScores []*UserScores `orm:"with:uid=id"` }
嵌套的模型也支持嵌套,只要是结构体嵌套的都支持自动数据赋值。例如:
type UserDetail struct { Uid int `json:"uid"` Address string `json:"address"` } type UserDetailEmbedded struct { UserDetail } type UserScores struct { Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } type User struct { *UserDetailEmbedded `orm:"with:uid=id"` Id int `json:"id"` Name string `json:"name"` UserScores []*UserScores `orm:"with:uid=id"` }
2、基础模型嵌套
type UserDetail struct { gmeta.Meta `orm:"table:user_detail"` Uid int `json:"uid"` Address string `json:"address"` } type UserScores struct { gmeta.Meta `orm:"table:user_scores"` Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } type UserEmbedded struct { Id int `json:"id"` Name string `json:"name"` } type User struct { gmeta.Meta `orm:"table:user"` UserEmbedded *UserDetail `orm:"with:uid=id"` UserScores []*UserScores `orm:"with:uid=id"` }
3、模型不带meta
信息
模型中的meta
结构重要的是指定数据表名称,当不存在meta
信息时,查询的数据表将会自动以结构体名称的CaseSnake
名称。例如,UserDetail
将会自动使用user_detail
数据表名称,UserScores
将会自动使用user_scores
数据表名称。
type UserDetail struct { Uid int `json:"uid"` Address string `json:"address"` } type UserScores struct { Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` } type User struct { *UserDetail `orm:"with:uid=id"` Id int `json:"id"` Name string `json:"name"` UserScores []*UserScores `orm:"with:uid=id"` }
七、后续改进
- 目前
With
特性仅实现了查询操作,还不支持写入更新等操作。
- No labels