一、设计背景
大家都知道易用性和易维护性一直是 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"`
}