GoFrame 模型關(guān)聯(lián)-With特性

2022-04-02 10:28 更新

一、設(shè)計(jì)背景

大家都知道易用性和易維護(hù)性一直是?goframe?一直努力建設(shè)的,也是?goframe?有別其他框架和組件比較大的一點(diǎn)差異。?goframe?沒有采用其他?ORM?常見的?BelongsTo?, ?HasOne?, ?HasMany?, ?ManyToMany?這樣的模型關(guān)聯(lián)設(shè)計(jì),這樣的關(guān)聯(lián)關(guān)系維護(hù)較繁瑣,例如外鍵約束、額外的標(biāo)簽備注等,對開發(fā)者有一定的心智負(fù)擔(dān)。因此框架不傾向于通過向模型結(jié)構(gòu)體中注入過多復(fù)雜的標(biāo)簽內(nèi)容、關(guān)聯(lián)屬性或方法,并一如既往地嘗試著簡化設(shè)計(jì),目標(biāo)是使得模型關(guān)聯(lián)查詢盡可能得易于理解、使用便捷。因此在之前推出了?ScanList?方案。

經(jīng)過一系列的項(xiàng)目實(shí)踐,我們發(fā)現(xiàn)?ScanList?雖然從運(yùn)行時(shí)業(yè)務(wù)邏輯的角度來維護(hù)了模型關(guān)聯(lián)關(guān)系,但是這種關(guān)聯(lián)關(guān)系維護(hù)也不如期望的簡便。因此,我們繼續(xù)改進(jìn)推出了可以通過模型簡單維護(hù)關(guān)聯(lián)關(guān)系的?With?模型關(guān)聯(lián)特性,當(dāng)然,這種特性仍然致力于提升整體框架的易用性和維護(hù)性,可以把?With?特性看做?ScanList?與模型關(guān)聯(lián)關(guān)系維護(hù)的一種結(jié)合和改進(jìn)。

?With?特性從?goframe v1.15.7?版本開始提供,目前屬于實(shí)驗(yàn)性特性。

二、舉個例子

我們先來一個簡單的示例,便于大家更好理解?With?特性,該示例來自于之前的?ScanList?章節(jié)的相同示例,改進(jìn)版。

1、數(shù)據(jù)結(jié)構(gòu)

# 用戶表
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;

# 用戶學(xué)分
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、數(shù)據(jù)結(jié)構(gòu)

根據(jù)表定義,我們可以得知:

  1. 用戶表與用戶詳情是?1:1?關(guān)系。
  2. 用戶表與用戶學(xué)分是?1:N?關(guān)系。
  3. 這里并沒有演示?N:N?的關(guān)系,因?yàn)橄啾容^于?1:N?的查詢只是多了一次關(guān)聯(lián)、或者一次查詢,最終處理方式和?1:N?類似。

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

// 用戶詳情
type UserDetail struct {
	gmeta.Meta `orm:"table:user_detail"`
	Uid        int    `json:"uid"`
	Address    string `json:"address"`
}
// 用戶學(xué)分
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、數(shù)據(jù)寫入

為簡化示例,我們這里創(chuàng)建5條用戶數(shù)據(jù),采用事務(wù)操作方式寫入:

  • 用戶信息,?id?為1-5,?name?為name_1到name_5。
  • 同時(shí)創(chuàng)建5條用戶詳情數(shù)據(jù),?address?數(shù)據(jù)為address_1到address_5。
  • 每個用戶創(chuàng)建5條學(xué)分信息,學(xué)分為1-5。
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 := UserScores{
				Uid:   int(lastInsertId),
				Score: j,
			}
			_, err = db.Model(userScore).Data(userScore).OmitEmpty().Insert()
			if err != nil {
				return err
			}
		}
	}
	return nil
})

執(zhí)行成功后,數(shù)據(jù)庫數(shù)據(jù)如下:

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、數(shù)據(jù)查詢

新的?With?特性下,數(shù)據(jù)查詢相當(dāng)簡便,例如,我們查詢一條數(shù)據(jù):

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

以上語句您將會查詢到用戶?ID?為3的用戶信息、用戶詳情以及用戶學(xué)分信息,以上語句將會在數(shù)據(jù)庫中自動執(zhí)行以下?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

執(zhí)行后,通過?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
db.Model(users).With(UserDetail{}).Where("id>?", 3).Scan(&users)

執(zhí)行后,通過?g.Dump(users)?打印用戶數(shù)據(jù)如下:

[
        {
                "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
        }
]

6、條件與排序

通過?With?特性關(guān)聯(lián)時(shí)可以指定關(guān)聯(lián)的額外條件,以及在多數(shù)據(jù)結(jié)果下指定排序規(guī)則。例如:

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?標(biāo)簽中的?where?子標(biāo)簽以及?order?子標(biāo)簽指定額外關(guān)聯(lián)條件體積排序規(guī)則。

三、詳細(xì)說明

想必您一定對上面的某些使用比較好奇,比如?gmeta?包、比如?WithAll?方法、比如?orm?標(biāo)簽中的?with?語句、比如?Model?方法給定?struct?參數(shù)識別數(shù)據(jù)表名等等,那這就對啦,接下來,我們詳細(xì)聊聊吧。

1、gmeta包

我們可以看到在上面的結(jié)構(gòu)體數(shù)據(jù)結(jié)構(gòu)中都使用?embed?方式嵌入了一個?gmeta.Meta?結(jié)構(gòu)體,例如:

type UserDetail struct {
	gmeta.Meta `orm:"table:user_detail"`
	Uid        int    `json:"uid"`
	Address    string `json:"address"`
}

其實(shí)在?GoFrame?框架中有很多這種小組件包用以實(shí)現(xiàn)特定的便捷功能。?gmeta?包的作用主要用于嵌入到用戶自定義的結(jié)構(gòu)體中,并且通過標(biāo)簽的形式給?gmeta?包的結(jié)構(gòu)體(例如這里的?gmeta.Meta?)打上自定義的標(biāo)簽內(nèi)容(列如這里的?`orm:"table:user_detail"`?),并在運(yùn)行時(shí)可以特定方法動態(tài)獲取這些自定義的標(biāo)簽內(nèi)容。

因此,這里嵌入?gmeta.Meta?的目的是為了標(biāo)記該結(jié)構(gòu)體關(guān)聯(lián)的數(shù)據(jù)表名稱。

2、模型關(guān)聯(lián)指定

在如下結(jié)構(gòu)體中:

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

我們通過給指定的結(jié)構(gòu)體屬性綁定?orm?標(biāo)簽,并在?orm?標(biāo)簽中通過?with?語句指定當(dāng)前結(jié)構(gòu)體(數(shù)據(jù)表)與目標(biāo)結(jié)構(gòu)體(數(shù)據(jù)表)的關(guān)聯(lián)關(guān)系,?with?語句的語法如下:

with:當(dāng)前屬性對應(yīng)表關(guān)聯(lián)字段=當(dāng)前結(jié)構(gòu)體對應(yīng)數(shù)據(jù)表關(guān)聯(lián)字段

并且字段名稱忽略大小寫以及特殊字符匹配,例如以下形式的關(guān)聯(lián)關(guān)系都是能夠自動識別的:

with:UID=ID
with:Uid=Id
with:U_ID=id

如果兩個表的關(guān)聯(lián)字段都是同一個名稱,那么也可以直接寫一個即可,例如:

with:uid

在本示例中,?UserDetail?屬性對應(yīng)的數(shù)據(jù)表為?user_detail?,?UserScores?屬性對應(yīng)的數(shù)據(jù)表為?user_score?,兩者與當(dāng)前?User?結(jié)構(gòu)體對應(yīng)的表?user?都是使用?uid?進(jìn)行關(guān)聯(lián),并且目標(biāo)關(guān)聯(lián)的?user?表的對應(yīng)字段為?id?。

3、With/WithAll

1、基本介紹

默認(rèn)情況下,即使我們的結(jié)構(gòu)體屬性中的?orm?標(biāo)簽帶有?with?語句,?ORM?組件并不會默認(rèn)啟用?With?特性進(jìn)行關(guān)聯(lián)查詢,而是需要依靠?With/WithAll?方法啟用該查詢特性。

  • ?With?:指定啟用關(guān)聯(lián)查詢的數(shù)據(jù)表,通過給定的屬性對象指定。
  • ?WithAll?:啟用操作對象中所有帶有?with?語句的屬性結(jié)構(gòu)體關(guān)聯(lián)查詢。

這兩個方法的定義如下:

// 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?表中的所有屬性的模型關(guān)聯(lián)查詢,只要屬性結(jié)構(gòu)體關(guān)聯(lián)了數(shù)據(jù)表,并且?orm?標(biāo)簽中帶有?with?語句,那么都將會自動查詢數(shù)據(jù)并根據(jù)模型結(jié)構(gòu)的關(guān)聯(lián)關(guān)系進(jìn)行數(shù)據(jù)綁定。假如我們只啟用某部分關(guān)聯(lián)查詢,并不啟用全部屬性模型的關(guān)聯(lián)查詢,那么可以使用?With?方法來指定。并且?With?方法可以指定啟用多個關(guān)聯(lián)模型的自動查詢,在本示例中的?WithAll?就相當(dāng)于:

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

也可以這樣:

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

2、僅關(guān)聯(lián)用戶詳情模型

假如我們只需要查詢用戶詳情,并不需要查詢用戶學(xué)分,那么我們可以使用?With?方法來啟用指定對象對應(yīng)數(shù)據(jù)表的關(guān)聯(lián)查詢,例如:

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

也可以這樣:

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

執(zhí)行后,通過?g.Dump(user)?打印用戶數(shù)據(jù)如下:

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

3、僅關(guān)聯(lián)用戶學(xué)分模型

我們也可以只關(guān)聯(lián)查詢用戶學(xué)分信息,例如:

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

也可以這樣:

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

執(zhí)行后,通過?g.Dump(user)?打印用戶數(shù)據(jù)如下:

{
        "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、不關(guān)聯(lián)任何模型查詢

假如,我們不需要關(guān)聯(lián)查詢,那么更簡單,例如:

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

執(zhí)行后,通過?g.Dump(user)?打印用戶數(shù)據(jù)如下:

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

四、使用限制

1、字段查詢與過濾

可以看到,在我們上面的示例中,并沒有指定查詢的字段,但是在打印的?SQL?日志中可以看到查詢語句不是簡單的?SELECT *?而是執(zhí)行了具體的字段查詢。在?With?特性下,將會自動按照關(guān)聯(lián)模型對象的屬性進(jìn)行查詢,屬性的名稱將會與數(shù)據(jù)表的字段做自動映射,并且會自動過濾掉無法自動映射的字段查詢。

所以,在?With?特性下,我們無法做到僅查詢屬性中對應(yīng)的某幾個字段。如果需要實(shí)現(xiàn)僅查詢并賦值某幾個字段,建議您對?model?數(shù)據(jù)結(jié)構(gòu)按照業(yè)務(wù)場景進(jìn)行裁剪,創(chuàng)建滿足特定業(yè)務(wù)場景的數(shù)據(jù)結(jié)構(gòu),而不是使用一個數(shù)據(jù)結(jié)構(gòu)滿足不同的多個場景。

我們來一個示例更好說明。假如我們有一個實(shí)體對象數(shù)據(jù)結(jié)構(gòu)?Content?,一個常見的?CMS?系統(tǒng)的內(nèi)容模型,與數(shù)據(jù)表字段一一對應(yīng):

type Content struct {
	Id             uint        `orm:"id,primary"       json:"id"`               // 自增ID
	Key            string      `orm:"key"              json:"key"`              // 唯一鍵名,用于程序硬編碼,一般不常用
	Type           string      `orm:"type"             json:"type"`             // 內(nèi)容模型: 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"`            // 標(biāo)題
	Content        string      `orm:"content"          json:"content"`          // 內(nèi)容
	Sort           uint        `orm:"sort"             json:"sort"`             // 排序,數(shù)值越低越靠前,默認(rèn)為添加時(shí)的時(shí)間戳,可用于置頂
	Brief          string      `orm:"brief"            json:"brief"`            // 摘要
	Thumb          string      `orm:"thumb"            json:"thumb"`            // 縮略圖
	Tags           string      `orm:"tags"             json:"tags"`             // 標(biāo)簽名稱列表,以JSON存儲
	Referer        string      `orm:"referer"          json:"referer"`          // 內(nèi)容來源,例如github/gitee
	Status         uint        `orm:"status"           json:"status"`           // 狀態(tài) 0: 正常, 1: 禁用
	ReplyCount     uint        `orm:"reply_count"      json:"reply_count"`      // 回復(fù)數(shù)量
	ViewCount      uint        `orm:"view_count"       json:"view_count"`       // 瀏覽數(shù)量
	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"`       // 創(chuàng)建時(shí)間
	UpdatedAt      *gtime.Time `orm:"updated_at"       json:"updated_at"`       // 修改時(shí)間
}

內(nèi)容的列表頁又不需要展示這么詳細(xì)的內(nèi)容,特別是其中的??Content??字段非常大,我們列表頁只需要查詢幾個字段而已。那么我們可以單獨(dú)定義一個用于列表的返回?cái)?shù)據(jù)結(jié)構(gòu),而不是直接使用數(shù)據(jù)表實(shí)體對象數(shù)據(jù)結(jié)構(gòu)。例如:

type ContentListItem struct {
	Id         uint        `json:"id"`          // 自增ID
	CategoryId uint        `json:"category_id"` // 欄目ID
	UserId     uint        `json:"user_id"`     // 用戶ID
	Title      string      `json:"title"`       // 標(biāo)題
	CreatedAt  *gtime.Time `json:"created_at"`  // 創(chuàng)建時(shí)間
	UpdatedAt  *gtime.Time `json:"updated_at"`  // 修改時(shí)間
}

2、必須存在關(guān)聯(lián)字段屬性

由于?With?特性是通過識別數(shù)據(jù)結(jié)構(gòu)關(guān)聯(lián)關(guān)系并自動執(zhí)行多條?SQL?查詢來實(shí)現(xiàn)的,因此關(guān)聯(lián)的字段也必須作為對象的屬性便于關(guān)聯(lián)字段值得自動獲取。簡單地講,?with?標(biāo)簽中的字段必須存在于關(guān)聯(lián)對象的屬性上。

五、后續(xù)改進(jìn)

目前?With?特性僅實(shí)現(xiàn)了查詢操作,還不支持寫入更新等操作。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號