Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

一、设计背景

大家都知道易用性和易维护性一直是goframe一直努力建设的,也是goframe有别其他框架和组件比较大的一点差异。goframe没有采用其他ORM常见的BelongsTo, HasOne, HasMany, ManyToMany这样的模型关联设计,这样的关联关系维护较繁琐,例如外键约束、额外的标签备注等,对开发者有一定的心智负担。因此框架不倾向于通过向模型结构体中注入过多复杂的标签内容、关联属性或方法,并一如既往地尝试着简化设计,目标是使得模型关联查询尽可能得易于理解、使用便捷。因此在之前推出了ScanList方案,建议大家在继续了解With特性之前先了解一下 模型关联-ScanList

经过一系列的项目实践,我们发现ScanList虽然从运行时业务逻辑的角度来维护了模型关联关系,但是这种关联关系维护也不如期望的简便。因此,我们继续改进推出了可以通过模型简单维护关联关系的With模型关联特性,当然,这种特性仍然致力于提升整体框架的易用性和维护性,可以把With特性看做ScanList与模型关联关系维护的一种结合和改进。

本特性需要感谢 aries 提供的宝贵建议。

Note

With特性从goframe v1.15.7版本开始提供,目前属于实验性特性。

二、先来示例

我们先来一个简单的示例,便于大家更好理解With特性,该示例来自于之前的ScanList章节的相同示例,改进版。

1、数据结构

Code Block
languagesql
# 用户表
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关系。
  2. 用户表与用户学分是1:N关系。
  3. 这里并没有演示N:N的关系,因为相比较于1:N的查询只是多了一次关联、或者一次查询,最终处理方式和1:N类似。

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

Code Block
languagego
// 用户详情
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条用户数据,采用事务操作方式写入:

  • 用户信息,id1-5namename_1name_5
  • 同时创建5条用户详情数据,address数据为address_1address_5
  • 每个用户创建5条学分信息,学分为1-5
Code Block
languagego
db.Transaction(func(tx *gdb.TX) error {
	for i := 1; i <= 5; i++ {
		// User.
		user := User{
			Name: fmt.Sprintf(`name_%d`, i),
		}
		lastInsertId, err := db.Model(user).Data(user).OmitEmpty().InsertAndGetId()
		if err != nil {
			return err
		}
		// Detail.
		userDetail := UserDetail{
			Uid:     int(lastInsertId),
			Address: fmt.Sprintf(`address_%d`, lastInsertId),
		}
		_, err = db.Model(userDetail).Data(userDetail).OmitEmpty().Insert()
		if err != nil {
			return err
		}
		// Scores.
		for j := 1; j <= 5; j++ {
			userScore := UserScore{
				Uid:   int(lastInsertId),
				Score: j,
			}
			_, err = db.Model(userScore).Data(userScore).OmitEmpty().Insert()
			if err != nil {
				return err
			}
		}
	}
	return nil
})

执行成功后,数据库数据如下:

Code Block
languagexml
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特性下,数据查询相当简便,例如,我们查询一条数据:

Code Block
languagego
var user *User
db.Model(tableUser).WithAll().Where("id", 3).Scan(&user)

以上语句您将会查询到用户ID为3的用户信息、用户详情以及用户学分信息,以上语句将会在数据库中自动执行以下SQL语句:

Code Block
languagego
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)打印的用户信息如下:

Code Block
languagejs
{
        "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
                }
        ]
}

想必您一定对上面的某些使用比较好奇,比如gmeta包、比如WithAll方法、比如orm标签中的with语句、比如Model方法给定struct参数识别数据表名等等,那这就对啦,接下来,我们详细聊聊吧。

三、详细说明

1、gmeta

我们可以看到在上面的结构体数据结构中都使用embed方式嵌入了一个gmeta.Meta结构体,例如:

Code Block
languagego
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、模型关联指定

在如下结构体中:

Code Block
languagego
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语句的语法如下:

Code Block
languagexml
with:当前属性对应表关联字段=当前结构体对应数据表关联字段

并且字段名称忽略大小写以及特殊字符匹配,例如以下形式的关联关系都是你能够自动识别字段的:,例如以下形式的关联关系都是能够自动识别的:

Code Block
languagexml
with:UID=ID
with:Uid=Id
with:U_ID=id

如果两个表的关联字段都是同一个名字,那么也可以直接写一个即可,例如:如果两个表的关联字段都是同一个名称,那么也可以直接写一个即可,例如:

Code Block
languagexml
with:uid

在本示例中,UserDetail属性对应的数据表为user_detailUserScores属性对应的数据表为user_score,两者与当前User结构体对应的表user都是使用uid进行关联,并且目标关联的user表的对应字段为id

3、With/WithAll

1)基本介绍

默认情况下,即使我们的结构体属性中的orm标签带有with语句,ORM组件并不会默认启用With特性进行关联查询,而是需要依靠With/WithAll方法启用该查询特性。

  • With:指定启用关联查询的数据表,通过给定的属性对象指定。
  • WithAll:启用操作对象中所有带有with语句的属性结构体关联查询。

这两个方法的定义如下:

Code Block
languagego
// With creates and returns an ORM model based on meta data of given object.
// It also enables model association operations feature on given `object`.
// It can be called multiple times to add one or more objects to model and enable
// their mode association operations feature.
// For example, if given struct definition:
// 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"`
// }
// We can enable model association operations on attribute `UserDetail` and `UserScores` by:
//     db.With(User{}.UserDetail).With(User{}.UserDetail).Scan(xxx)
// Or:
//     db.With(UserDetail{}).With(UserDetail{}).Scan(xxx)
// Or:
//     db.With(UserDetail{}, UserDetail{}).Scan(xxx)
func (m *Model) With(objects ...interface{}) *Model

// WithAll enables model association operations on all objects that have "with" tag in the struct.
func (m *Model) WithAll() *Model

在我们本示例中,使用的是WithAll方法,因此自动启用了User表中的所有属性的模型关联查询,只要属性结构体关联了数据表,并且orm标签中带有with语句,那么都将会自动查询数据并根据模型结构的关联关系进行数据绑定。假如我们只启用某部分关联查询,并不启用全部属性模型的关联查询,那么可以使用With方法来指定。并且With方法可以指定启用多个关联模型的自动查询,在本示例中的WithAll就相当于:

Code Block
languagego
var user *User
db.Model(tableUser).With(UserDetail{}, UserScore{}).Where("id", 3).Scan(&user)
2)仅关联用户详情模型

也可以这样:

假如我们只需要查询用户详情,并不需要查询用户学分,那么我们可以使用With方法来启用指定对象对应数据表的关联查询,例如:

Code Block
languagego
var user *User
db.Model(tableUser).With(User{}.UserDetail, User{}.UserScore).Where("id", 3).Scan(&user)
执行后,通过g.Dump(user)打印用户数据如下:

2)仅关联用户详情模型

假如我们只需要查询用户详情,并不需要查询用户学分,那么我们可以使用With方法来启用指定对象对应数据表的关联查询,例如:

Code Block
languagejsgo
{
var        user *User
db.Model(tableUser).With(UserDetail{}).Where("id": 3,
  , 3).Scan(&user)

也可以这样:

Code Block
languagego
var user *User
db.Model(tableUser).With(User{}.UserDetail).Where("id", 3).Scan(&user)

执行后,通过g.Dump(user)打印用户数据如下:

Code Block
languagejs
{
        "id": 3,
        "name": "name_3",
        "UserDetail": {
                "uid": 3,
                "address": "address_3"
        },
        "UserScores": null
}

3)仅关联用户学分模型

我们也可以只关联查询用户学分信息,例如:

Code Block
languagego
var user *User
db.Model(tableUser).With(UserScore{}).Where("id", 3).Scan(&user)

也可以这样:

Code Block
languagego
var user *User
db.Model(tableUser).With(User{}.UserScore).Where("id", 3).Scan(&user)

执行后,通过g.Dump(user)打印用户数据如下:

Code Block
languagejs
{
        "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)不关联任何模型查询

假如,我们不需要关联查询,那么更简单,例如:

Code Block
languagego
var user *User
db.Model(tableUser).Where("id", 3).Scan(&user)

执行后,通过g.Dump(user)打印用户数据如下:

Code Block
languagejs
{
        "id": 3,
        "name": "name_3",
        "UserDetail": null,
        "UserScores": null
}

4、当然支持列表查询

我们来一个通过With特性查询列表的示例:

Code Block
languagego
var users []*User
db.Model(users).With(UserDetail{}).Where("id>?", 3).Scan(&users)

执行后,通过g.Dump(users)打印用户数据如下:

Code Block
languagejs
[
        {
                "id": 4,
                "name": "name_4",
                "UserDetail": {
                        "uid": 4,
                        "address": "address_4"
                },
                "UserScores": null
        },
        {
                "id": 5,
                "name": "name_5",
                "UserDetail": {
                        "uid": 5,
                        "address": "address_5"
                },
                "UserScores": null
        }
]

四、后续改进

  • 目前With特性仅实现了查询操作,还不支持写入更新等操作。






















Panel
titleContent Menu

Table of Contents