一、设计背景

大家都知道易用性和易维护性一直是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关系。
  2. 用户表与用户学分是1:N关系。
  3. 这里并没有演示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条用户数据,采用事务操作方式写入:

  • 用户信息,id1-5namename_1name_5
  • 同时创建5条用户详情数据,address数据为address_1address_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_detailUserScores属性对应的数据表为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特性仅实现了查询操作,还不支持写入更新等操作。





















Content Menu

  • No labels

58 Comments

  1. xx

    db.Model(tableUser).WithAll().Where("id", 3).Scan(&user)

    这个tableUser是什么?

    1. xx

      哎,是User{}user

  2. gmeta.Meta `orm:"table:库名.user"`

    是否支持跨数据库使用,有没有其他处理方案。直接拼接上库名会将Meta作为字段查询  SELECT `Meta`,   

    1. 暂不支持跨数据库,你可以在github上提个issue回头我考虑下。

  3. xx

    type User struct {
        gmeta.Meta `orm:"table:user"`
        Id         int          `json:"id"`
        Name       string       `json:"name"`
        UserDetail  `orm:"with:uid=id"`
     }

    UserDetail  这样的结构体匿名字段,User打印不出来UserDetail里面包含的字段;因为很多时候想要把UserDetail 里面的字段放在User里的最外层

    1. 我没试过嵌套使用with,如果不行的话你可以提个issue记录一下。

  4. xx

    能够多字段关联吗?比如:

    UserDetail *UserDetail   `orm:"with:uid=id|with:name=addName"`
    1. 不支持,表与表之间只能使用一个字段关联。

      1. xx

        那如果有多个关联到就得拿发回的数据再做一次for循环筛选,能不能加个筛选条件的地方,比如一个表某个子段值等于关联的表某个字段值,或者被关联的表的某个字段值等于指定值才返回结果

        1. 你这个需求感觉可以使用ScanList模型关联-动态关联-ScanList

          1. xx

            不是的,只是通常的联表多个条件关联查询而已,ScanList不满足,ScanList的关联方式太简单了不够通用

  5. //gf gen dao自动生成的模型
    type Users internal.Users
    
    // 能正常加载关联
    type UserOrder struct {
    	Id uint64 `orm:"id,primary" json:"id"` //
    	Nickname string `orm:"nickname" json:"nickname"` //
    	Username string `orm:"username" json:"username"` //
    	Password string `orm:"password" json:"password"` //
    	CreatedAt *gtime.Time `orm:"created_at" json:"createdAt"` //
    	UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt"` //
    	Orders []*Orders `orm:"with:user_id=id" json:"orders"`
    }
    
    
    // 不能加载关联
    type UserOrder struct {
    	*Users
    	Orders []*Orders `orm:"with:user_id=id" json:"orders"`//另外一个基于gf gen dao生成的模型
    }
    1. 是的,目前只有自定义的结构体才支持,不支持自动读取表与表关联关系,下个版本的CLI工具链会支持自动获取。

      1. 不这样做就要反复定义entity了,求支持

        1. ysxpark willem 大家好,我看了下,最新版本的话这种关联是支持的,大家可以再试试,具体参考单例:https://github.com/gogf/gf/blob/32f33b9f8c08fc18385ea7ebc863e80c8d0e31cd/database/gdb/gdb_z_mysql_feature_with_test.go#L1052

  6. type CategoryArtModel struct {
    	gmeta.Meta `orm:"table:categorys"`
    	Id         uint              `orm:"id,primary" json:"id"`            //
    	Name       string            `orm:"name,unique" json:"name"`         // 分类名
    	ParentId   uint              `orm:"parent_id"`                       // 父级分类id
    	ParentCate *CategoryArtModel `json:"parent" orm:"with:id=parent_id"` // 获取父级分类
    }

    想问问大家, 像这样的model, 它会以ParentId的值一直向上获取父级的信息, 但是ParentId是有默认0的,  orm:"with:id=parent_id"程序

    好像会一直以id=0一直向数据库进行查询, 请问, 有什么办法能让它ParentId==0时终止查询之类的吗,

    或者是不是我这样的做法不对, 新手, 请多多赐教


    1. 你没有使用With*方法是不会执行关联查询的。另外你看看文档的With方法介绍。

      1. 啊不好意思,可能我没讲清楚,我刚发现是我的别的地方出了一点错,修改后已经能正常使用了。

        虽然怪不好意思的但还是另外有个地方想请教您: With 是不是只能查一层的数据的, 还是说是我的代码又又有问题亚:

        (评论列表, 每个评论还有子评论列表, 子评论又有孙子评论列表。。。这样下去),

        我这好像只能往下查一层:

        {
                 "id": 1,
                 "content": "testtesttest",
                 "ParentId": 0,
                 "SubComments": [
                          {
                                 "id": 5,
                                 "content": "sdgsadg",
                                 "ParentId": 1,
                                "SubComments": null   # parent_id == 5的评论不能获取解析到这里。。。
                           },
                          {
                                 "id": 9,
                                 "content": "dasgsdag",
                                 "ParentId": 1,
                                 "SubComments": null
                           },。。。

                   ]
        }

        这是我的model:

        type CommentTest2 struct {
        gmeta.Meta                                     `orm:"table:comments"`
        Id                      uint                          `orm:"id,primary" json:"id"`
        Content            string                      `orm:"content" json:"content"`
        ParentId           uint                         `orm:"parent_id"`
        SubComments []*CommentTest2 `orm:"with:parent_id=id" json:"SubComments"` //子评论列表,条件:子评论的parentid==当前id
        }

        这是我查询数据库的代码

        func (art *article) Test() []*model.CommentTest2 {
                 var CommentList []*model.CommentTest2
                 dao.Comments.WherePri(1).WithAll().Scan(&CommentList) //test拿id==1的评论以及它的子/孙评论门
                 return CommentList
        }

        请问我应该修改哪些

        1. 可以无限嵌套的,不过我只测试过一层的。如果有问题可以把可执行的代码和相关SQL提到github issue上,我运行试试。

  7. 请问当我使用with模型关联的时候,比如您提交的例子,用户表,关联了用户学分,当我with用户学分模型的时候,这个学分模型如何加Where条件呢,比如查询用户列表,然后关联用户学分,其中关联的学分数据这一部分我只需要学分大于2的这种,请问一下这种情况该如何写啊

    1. 暂不支持静态关联模型还带额外的条件,你可以提个issue记录下后续改进。

  8. hyh

    WithAll()查询时,如何对子表指定Order?

    1. 目前不支持,下个版本支持。

  9. 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"`  // 修改时间
    }

    在字段过滤这一块,一定要写上关联的userid吗,因为最后列表中可能只需要标题,修改时间这两个内容字段就够了。而userid只是用来关联用的,可这样子最后查出来后,我返回给前端,是会带上userid的,但我作为后端自然是不想要他知道这个userid。

    能否在结构体使用定义上,无需强加关联id,方便最后查询出来后直接返回给前端需要看到的字段

    1. 关联与返回的逻辑是独立的,是不同的组件控制的。你可以通过`json:"-"`的标签来设置不返回该字段。

  10. 如何在关联的子表上使用sum、 count、 contact等方法?谢谢

    1. 目前不支持复杂的自动子查询,建议业务上分开自行查询,便于管理维护。

  11. 请问目前有增加复合主键的计划吗

    1. 请提issue描述记录。

  12. 这样操作,造成死循环查询:

    type User struct {
       gmeta.Meta `orm:"table:user"`
       ID         int           `orm:"id"`
       Name       string        `orm:"name"`
       Password   string        `orm:"password"`
       View       int           `orm:"view"`
       CreatedAt  *gtime.Time   `orm:"created_at"`
       UpdatedAt  *gtime.Time   `orm:"updated_at"`
       UserDetail *UserDetail   `orm:"with:user_id=id"`
       UserScores []*UserScores `orm:"with:user_id=id"`
    }
    
    type UserDetail struct {
       gmeta.Meta `orm:"table:user_detail"`
       UserId     int    `orm:"user_id"`
       Address    string `orm:"address"`
       User       *User  `orm:"with:id=user_id"`
    }
    
    
    
    var userDetail *entity.UserDetail
    err := g.Model("user_detail").Where("user_id", 1).WithAll().Scan(&userDetail)
    // err := g.Model("user_detail").Where("user_id", 1).With(entity.User{}).Scan(&userDetail)
    if err != nil {
       panic(err)
    }
    g.Dump(userDetail)

    这里的WithAll造成嵌套with,死循环执行查询。

    1. issue反馈后不久已解决。

  13. 郭强 with多个的话, 出现了概率性能查得到
    查看sql日志 会发现关联的某些sql语句没有执行的情况

  14. ScanList挺好用的, 可以直接使用 框架生成的 do。如下面的代码

    type IndexBook struct {
    	Book     do.Book
    	Category do.BookCategory
    	Writer   do.BookWriter
    }
    
    
    func IndexRecommend(ctx context.Context) ([]*bo.IndexBook, error) {
    	var d = make([]*bo.IndexBook, 0)
    	if err := dao.Book.Ctx(ctx).Where("recommend = 1").ScanList(&d, "Book"); err != nil {
    		return nil, err
    	}
    	if err := dao.BookCategory.Ctx(ctx).ScanList(&d, "Category", "Book", "Id:CategoryId"); err != nil {
    		return nil, err
    	}
    	if err := dao.BookWriter.Ctx(ctx).ScanList(&d, "Writer", "Book", "Id:WriterId"); err != nil {
    		return nil, err
    	}
    	return d, nil
    }

    但 with特性需要自己配置更多的关联关系,而且必须加

    gmeta.Meta `orm:"table:user_detail"`

    感觉不是很好用。

    不过更期待大佬的 分表查询特性的出现。

  15. type Channel struct {
    	g.Meta   `orm:"table:channel"`
    	Id       uint     `json:"id"       description:""`
    	Name     string   `json:"name"     description:"渠道名称"`
    	Code     string   `json:"code"     description:"渠道码"`
    	ParentId int      `json:"parent_id" description:"父渠道"`
    	Parent   *Channel `json:"parent" orm:"with:id=parent_id" description:"父渠道信息"`
    }

    像上述model定义的话会造成死循环,如下

    2022-08-29 14:40:59.977 [DEBU] {f871e6ee95bd0f1791c3487d7f928d6e} [ 30 ms] [default] [rows:0  ] SELECT `id`,`name`,`code`,`parent_id` FROM `channel` WHERE `id`=0 LIMIT 1
    2022-08-29 14:40:59.990 [DEBU] {f871e6ee95bd0f1791c3487d7f928d6e} [ 13 ms] [default] [rows:0  ] SELECT `id`,`name`,`code`,`parent_id` FROM `channel` WHERE `id`=0 LIMIT 1
    2022-08-29 14:41:00.004 [DEBU] {f871e6ee95bd0f1791c3487d7f928d6e} [ 14 ms] [default] [rows:0  ] SELECT `id`,`name`,`code`,`parent_id` FROM `channel` WHERE `id`=0 LIMIT 1
    2022-08-29 14:41:00.018 [DEBU] {f871e6ee95bd0f1791c3487d7f928d6e} [ 14 ms] [default] [rows:0  ] SELECT `id`,`name`,`code`,`parent_id` FROM `channel` WHERE `id`=0 LIMIT 1
    2022-08-29 14:41:00.030 [DEBU] {f871e6ee95bd0f1791c3487d7f928d6e} [ 11 ms] [default] [rows:0  ] SELECT `id`,`name`,`code`,`parent_id` FROM `channel` WHERE `id`=0 LIMIT 1
    2022-08-29 14:41:00.068 [DEBU] {f871e6ee95bd0f1791c3487d7f928d6e} [ 37 ms] [default] [rows:0  ] SELECT `id`,`name`,`code`,`parent_id` FROM `channel` WHERE `id`=0 LIMIT 1
    2022-08-29 14:41:00.094 [DEBU] {f871e6ee95bd0f1791c3487d7f928d6e} [ 26 ms] [default] [rows:0  ] SELECT `id`,`name`,`code`,`parent_id` FROM `channel` WHERE `id`=0 LIMIT 1

    这种情况该如何处理呢?如果关联的是1对多 一个数组类型的就不会造成死循环情况,如下

    type BookCommentItem struct {
    	g.Meta      `orm:"table:book_comments"`
    	Id          int                `json:"id"                description:"评论ID"`
    	UserId      int                `json:"-"           description:"评论ID"`
    	ReplyId     int                `json:"-"           description:"回复ID"`
    	LikeNum     int                `json:"like_num"           description:"点赞数"`
    	Content     string             `json:"content"              description:"评论内容"`
    	CreatedTime int64              `json:"publish_time"       description:"发表时间"`
    	User        *UserInfo          `json:"user"        orm:"with:id=user_id"       description:"用户"`
    	ReplyList   []*BookCommentItem `json:"reply_lists"  orm:"with:reply_id=id"     description:"回复"`
    }

    郭强 

  16. Ray

    我想问下 这些关联写在哪里呢 在 model 新建个文件 写吗  是不是不能写在 entiy 生成文件

    郭强 

  17. 递归关联到第三层之后 会重复执行多条sql

    目前的表关系  主订单表关联-》[]*子订单表→*商品表

    // ResGridOrderGridInfo 订单列表
    type ResGridOrderGridInfo struct {
    	gmeta.Meta         `orm:"table:shop_order"`
    	OrderId            int64                     `json:"orderId"         description:"主订单ID"`
    	OrderDetails       []*ResShopOrderDetailInfo `json:"orderDetails" orm:"with:order_id=order_id" `
    	MerchantInfo       *ResShopOrderMerchantInfo `json:"merchantInfo" orm:"with:merchant_id=merchant_id" `
    }
    
    // ResShopOrderDetailInfo 子订单详情
    type ResShopOrderDetailInfo struct {
    	gmeta.Meta      `orm:"table:shop_order_detail"`
    	OrderId         int64                    `json:"orderId"         description:"主订单ID"`
    	OrderDetailCode string                   `json:"orderDetailCode" description:"子单号"`
    	ProId           int64                    `json:"proId"           description:"商品ID"`
    	ProDuctInfo     *ResShopOrderProductInfo `json:"proDuctInfo" orm:"with:pro_id=pro_id" `
    }
    
    // ResShopOrderProductInfo 商品数据详情
    type ResShopOrderProductInfo struct {
    	gmeta.Meta   `orm:"table:shop_product"`
    	ProId        int     `json:"proId"   dc:"商品ID"`
    	ProName      string  `json:"proName"   dc:"商品名称"`
    }
    
    type ResShopOrderMerchantInfo struct {
    	gmeta.Meta      `orm:"table:shop_merchant"`
    	MerchantId      int    `json:"merchantId"         description:"商家ID"`
    }
    
    type OrderGridRes struct {
    	gmeta.Meta `orm:"table:shop_order"`
    	Grid       []api.ResGridOrderGridInfo `json:"grid" dc:"订单列表"`
    	Total      int                        `json:"total" dc:"订单列表总数"` 
    }
    
    //start 
    _ = db.Model(res).WithAll().Scan(&OrderGridRes.Grid)
    
    


    运行后 有多少个子订单就会查询多少次 商品表数据

    2022-10-09 17:56:55.113 [DEBU] {70bac1ad4c591c1726535d347dbec5c9} [117 ms] [default] [rows:1  ] SELECT `pro_id`,`pro_name`,`pro_head_img`,`pro_price`,`pro_sale_price`,`freight_price` FROM `shop_product` WHERE `pro_id` IN(1)
    2022-10-09 17:56:55.186 [DEBU] {70bac1ad4c591c1726535d347dbec5c9} [ 72 ms] [default] [rows:1  ] SELECT `pro_id`,`pro_name`,`pro_head_img`,`pro_price`,`pro_sale_price`,`freight_price` FROM `shop_product` WHERE `pro_id` IN(1)
    2022-10-09 17:56:55.263 [DEBU] {70bac1ad4c591c1726535d347dbec5c9} [ 77 ms] [default] [rows:1  ] SELECT `pro_id`,`pro_name`,`pro_head_img`,`pro_price`,`pro_sale_price`,`freight_price` FROM `shop_product` WHERE `pro_id` IN(1)
    2022-10-09 17:56:55.370 [DEBU] {70bac1ad4c591c1726535d347dbec5c9} [106 ms] [default] [rows:1  ] SELECT `pro_id`,`pro_name`,`pro_head_img`,`pro_price`,`pro_sale_price`,`freight_price` FROM `shop_product` WHERE `pro_id` IN(1)



  18. hui

    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"`
    }

    目前通过 tag 指定 where 查询条件的方式,如果参数是动态变化的貌似无法实现?比如 where: score>${score}

    1. 遇见同样的问题,如果能动态传参就好了

  19. 请问使用模型关联时,匿名结构体,自定义的结构体名称能与orm的tag名称不一样吗,

    所关联两个表的字段都叫Code和Name, 这样写被关联的表的相同字段会被覆盖, 有没有解决办法.

    DeptWithListOutPut struct {
    	gmeta.Meta           `orm:"table:bd_dept"`
    	*OrgCodeAndNameIntro `orm:"with:org_id"`
    	DeptBaseShared (表的其他字段,包含OrgId,Code,Name)
    }
    
    OrgCodeAndNameIntro struct {
    	gmeta.Meta `orm:"table:sys_org"`
    	OrgId   uint   `json:"orgId"`              // 组织ID
    	OrgCode string `orm:"code" json:"orgCode"` // 编码
    	OrgName string `orm:"name" json:"orgName"` // 名称
    }
  20. xiu

    嵌套后sql多次执行,希望可以一起id in后再处理数据:

    SELECT `id`,`uid`,`name`,`state`,`tp` FROM `other_optional_name` WHERE `id` IN(422669)
    SELECT `id`,`uid`,`name`,`state`,`tp` FROM `other_optional_name` WHERE `id` IN(384597,470068)
  21. 触发递归关联的情况下,如何设置停止点,比如:
    我现在有一个分类表

    type Category struct {
    	Id        uint        //
    	ParentId  uint        // 上级ID
    	Path      uint        // 层级
    	Sort      uint        // 排序
    	Name      string      // 分类名
    	CreatedAt *gtime.Time //
    	UpdatedAt *gtime.Time //
    	DeletedAt *gtime.Time //
    }
    
    type CategoryWithParent struct {
    	g.Meta `orm:"table:category"`
    	entity.Category
    	Parent *CategoryWithParent `orm:"with:id=parent_id"`
    }
    
    var category model.CategoryWithParent
    err = dao.Category.Ctx(ctx).
    	Where("id = ?", 1).
    	WithAll().
    	Scan(&category)
    if err != nil {
    	return err
    }

    当我如上编写代码时,递归到parent_id的值为0,即不再有父级的时候,orm依然会把id=0作为条件继续查询,并无限循环

    请问这种情况如何解决,还是说目前版本的orm无法解决?

  22. 牛掰, 感觉这个静态关联更加易懂.

  23. // BackFrontUser is the golang structure for table back_front_user.
    type BackFrontUser struct {
    Id int64 `orm:"id" json:"id" ` //
    BackUserId int64 `orm:"back_user_id"json:"back_user_id" ` // 后台用户表id
    UserId int64 `orm:"user_id" json:"user_id" ` // 前台用户表id
    ParentId int64 `orm:"parent_id" json:"parent_id" ` // 父级id
    RoleId uint `orm:"role_id" json:"roleId" ` // 角色id1总代|2团长|3团员
    InvitationCode string `orm:"invitation_code" json:"invitation_code" ` // 前台用户邀请码
    CreatedAt *gtime.Time `orm:"created_at"json:"created_at" ` // 创建时间
    UpdatedAt *gtime.Time `orm:"updated_at"json:"updatedAt" ` // 更新时间
    DeletedAt *gtime.Time `orm:"deleted_at"json:"deleted_at" ` // 删除时间
    //SonUserInfo []*BackFrontUser `orm:"with:parent_id=id" json:"son_user_info"`
    UserInfo *CommonStarUser `orm:"with:id=user_id" json:"user_info"`
    }

    type CommonStarUser struct {
    Id int64 `json:"id" ` //
    UserName string `json:"userName" ` // 用户名
    Md5Password string `json:"md5Password" ` // md5密码
    Tel string `json:"tel" ` // 手机号
    CreatedAt *gtime.Time `json:"createdAt" ` // 创建时间
    UpdatedAt *gtime.Time `json:"updatedAt" ` // 更新时间
    DeletedAt *gtime.Time `json:"deletedAt" ` // 删除时间
    }

    // pid就是传userid
    func GetRoles(pid int64, m *[]*model.BackFrontUser) {
    proles := make([]*model.BackFrontUser, 0)
    g.Model("back_front_user").With(model.CommonStarUser{}).
    Where("parent_id", pid).Scan(&proles)

    for _, item := range proles {
    *m = append(*m, &model.BackFrontUser{
    Id: item.Id,
    BackUserId: item.BackUserId,
    UserId: item.UserId,
    ParentId: item.ParentId,
    UserInfo: &model.CommonStarUser{
    Tel: item.UserInfo.Tel,
    UserName: item.UserInfo.UserName,
    },
    })
    GetRoles(item.ParentId, m)
    }

    }



    递归的方式查询无限父级角色下所有成员,执行陷入无限循环

    1. 递归查询无线下级可以的,不用GetRoles这样递归,在struct里面递归

  24. 大佬,可否考虑关联条件动态传参?with()里面传递参数过来

  25. 有个极为常见的例子  id和name互转   可以在字段后面直接关联吗    现在可能要多个结构体才可以

    假设一个动物详细表  里面有各种动物       然后有个天敌表  存在所有的天敌关系   

    我要查询羊的所有天敌     那么查询出来的肯定是天敌的ID  然后天敌的名字又存放在 野兽表里

    这样写  期望能有个方案 解决这个常用场景   ID Name转换  

     

    1. 数据圆环造成的死循环 A>B>C>A

      1. 这样并没有死循环  只是不方便 是  A动物结构体包含B天敌结构体   B天敌包含的是另外一个结构体  C天敌名字

        1. 假设张三怕李四怕赵五怕张三, 获取列表得到三行(animal),触发with查到对应天敌行(enemy),天敌行(enemy)调用(animal)又触发with无限循环.

    2. 参考go的循环依赖, type A包含type B,type B又包含type A ,这样的结构设计就是有问题的

      1. 嗯 这样是不行的,  所以我用了另外一个结构体  来解决  只是为了一个字段 专门建立个结构体  太不方便

        1. 调用的时候  调用AnimalEnemy.EnemyName.Name也非常的不优雅

            1. 好的   那字段冲突  有什么好的解决方案吗   他们两个都存在一个ID字段

        2. 建立一个新的就不会死循环的原因是EnemyName没有with关系,只是一个数据填充,如果把enemy的with也置入到EnemyName中一样死循环.目前with特性无法设定loop深度,只能自己注意.

  26. 这里的定义一执行GF工具不会覆盖吗?

    1. 不用直接修改do或entity文件,可以新建自定义关联模型grinning face 

  27. 能否给个 N:N的例子?