Go 數(shù)據(jù)存儲(chǔ)篇(六):數(shù)據(jù)表之間的關(guān)聯(lián)關(guān)系和關(guān)聯(lián)查詢
1、關(guān)聯(lián)關(guān)系簡(jiǎn)介
MySQL 之所以被稱之為關(guān)系型數(shù)據(jù)庫(kù),是因?yàn)榭梢曰谕怄I定義數(shù)據(jù)表之間的關(guān)聯(lián)關(guān)系,日常開(kāi)發(fā)常見(jiàn)的關(guān)聯(lián)關(guān)系如下所示:
一對(duì)一:一張表的一條記錄對(duì)應(yīng)另一張表的一條記錄,比如用戶表與用戶資料表
一對(duì)多:一張表的一條記錄對(duì)應(yīng)另一張表的多條記錄,比如用戶表與文章表、文章表與評(píng)論表
多對(duì)一:一張表的多條記錄歸屬另一張表的一條記錄(一對(duì)多的逆向操作)
多對(duì)多:一張表的多條記錄歸屬另一張表的多條記錄,此時(shí)僅僅基于兩張表的字段已經(jīng)無(wú)法定義這種關(guān)聯(lián)關(guān)系,需要借助中間表來(lái)定義,比如文章表與標(biāo)簽表往往是這種關(guān)聯(lián)
我們?cè)谏掀坛桃呀?jīng)介紹了 Go 語(yǔ)言中基于第三方包 go-sql-driver/mysql 對(duì)單張數(shù)據(jù)表的增刪改查操作,接下來(lái)我們來(lái)看看如何基于這個(gè)包對(duì)關(guān)聯(lián)表進(jìn)行操作。
2、新建評(píng)論表
為了方便演示,我們?cè)?test_db 數(shù)據(jù)庫(kù)中新建一張?jiān)u論表 comments:
CREATE?TABLE?`comments`?(
??`id`?bigint?unsigned?NOT?NULL?AUTO_INCREMENT,
??`content`?text?COLLATE?utf8mb4_unicode_ci,
??`author`?varchar(30)?COLLATE?utf8mb4_unicode_ci?DEFAULT?NULL,
??`post_id`?bigint?unsigned?DEFAULT?NULL,
??PRIMARY?KEY?(`id`),
??FOREIGN?KEY?fk_post_id(`post_id`)?REFERENCES?posts(`id`)?ON?DELETE?CASCADE
)?ENGINE=InnoDB?DEFAULT?CHARSET=utf8mb4?COLLATE=utf8mb4_unicode_ci;
這里我們創(chuàng)建了一個(gè)外鍵將 comments 表的 post_id 字段和 posts 表的 id 字段關(guān)聯(lián)起來(lái),并且通過(guò) ON DELETE CASCADE 聲明將兩張表級(jí)聯(lián)起來(lái):當(dāng)刪除 posts 表中的某條記錄時(shí),自動(dòng)刪除 comments 中與之關(guān)聯(lián)的評(píng)論記錄(如果省略這個(gè)聲明,則不能直接刪除 posts 表中有 comments 關(guān)聯(lián)依賴的記錄)。
我們?cè)?posts 和 comments 插入兩條記錄,這兩條記錄通過(guò) comments.post_id 建立了外鍵關(guān)聯(lián):


此時(shí),如果刪除 posts 表中的記錄,刷新 comments 表,會(huì)發(fā)現(xiàn) comments 表對(duì)應(yīng)記錄也被清空,說(shuō)明外鍵關(guān)聯(lián)生效。
3、編寫(xiě)示例代碼
接下來(lái),我們編寫(xiě)一段示例代碼演示如何在 Go 語(yǔ)言中通過(guò) go-sql-driver/mysql 包對(duì)文章表和評(píng)論表進(jìn)行關(guān)聯(lián)查詢。
新建一個(gè) mysql 子目錄來(lái)存放示例代碼,這一次,我們通過(guò)拆分不同操作業(yè)務(wù)邏輯到不同文件來(lái)構(gòu)建這個(gè)示例程序。
初始化連接
在 mysql 目錄下新建一個(gè) conn.go 編寫(xiě)數(shù)據(jù)庫(kù)連接代碼:
package?main
import?(
????"database/sql"
????_?"github.com/go-sql-driver/mysql"
)
var?Db?*sql.DB
//?初始化數(shù)據(jù)庫(kù)連接
func?init()?{
????var?err?error
????Db,?err?=?sql.Open("mysql",?"root:root@/test_db?charset=utf8mb4&parseTime=true")
????if?err?!=?nil?{
????????panic(err)
????}
}
注意到 Db 變量首字母大寫(xiě)了,因此一旦初始化之后,就可以在當(dāng)前包下的任何文件中直接引用了。
遷移文章增刪改查代碼
將 posts 表增刪改查操作拆分到獨(dú)立的 post.go,并且在 Post 結(jié)構(gòu)體中引入 Comments []Comment 屬性存放關(guān)聯(lián)的評(píng)論信息:
package?main
type?Post?struct?{
????Id?int
????Title?string
????Content?string
????Author?string
????Comments?[]Comment
}
func?Posts(limit?int)?(posts?[]Post,?err?error)?{
????stmt,?err?:=?Db.Prepare("select?id,?title,?content,?author?from?posts?limit??")
????if?err?!=?nil?{
????????panic(err)
????}
????defer?stmt.Close()
????rows,?err?:=?stmt.Query(limit)
????if?err?!=?nil?{
????????panic(err)
????}
????for?rows.Next()?{
????????post?:=?Post{}
????????err?=?rows.Scan(&post.Id,?&post.Title,?&post.Content,?&post.Author)
????????if?err?!=?nil?{
????????????panic(err)
????????}
????????posts?=?append(posts,?post)
????}
????return
}
func?GetPost(id?int)?(post?Post,?err?error)?{
????post?=?Post{}
????err?=?Db.QueryRow("select?id,?title,?content,?author?from?posts?where?id?=??",?id).
????????Scan(&post.Id,?&post.Title,?&post.Content,?&post.Author)
????//?查詢與之關(guān)聯(lián)的?comments?記錄
????rows,?err?:=?Db.Query("select?id,?content,?author?from?comments?where?post_id?=??",?post.Id)
????for?rows.Next()?{
????????comment?:=?Comment{Post:?&post}
????????err?=?rows.Scan(&comment.Id,?&comment.Content,?&comment.Author)
????????if?err?!=?nil?{
????????????return
????????}
????????post.Comments?=?append(post.Comments,?comment)
????}
????rows.Close()
????return
}
func?(post?*Post)?Create()?(err?error)?{
????sql?:=?"insert?into?posts?(title,?content,?author)?values?(?,??,??)"
????stmt,?err?:=?Db.Prepare(sql)
????if?err?!=?nil?{
????????panic(err)
????}
????defer?stmt.Close()
????res,?err?:=?stmt.Exec(post.Title,?post.Content,?post.Author)
????if?err?!=?nil?{
????????panic(err)
????}
????postId,?_?:=?res.LastInsertId()
????post.Id?=?int(postId)
????return
}
func?(post?*Post)?Update()?(err?error)??{
????stmt,?err?:=?Db.Prepare("update?posts?set?title?=??,?content?=??,?author?=???where?id?=??")
????if?err?!=?nil?{
????????return
????}
????stmt.Exec(post.Title,?post.Content,?post.Author,?post.Id)
????return
}
func?(post?*Post)?Delete()?(err?error)?{
????stmt,?err?:=?Db.Prepare("delete?from?posts?where?id?=??")
????if?err?!=?nil?{
????????return
????}
????stmt.Exec(post.Id)
????return
}
我們?cè)?GetPost 方法中獲取單條文章記錄后,再通過(guò)對(duì)應(yīng)文章 ID 進(jìn)行數(shù)據(jù)庫(kù)查詢獲取相關(guān)評(píng)論信息存放到 post 對(duì)象的 Comments 屬性中,這樣就可以通過(guò)該屬性獲取文章的評(píng)論數(shù)據(jù)了。
定義評(píng)論相關(guān)操作
緊接著創(chuàng)建 comment.go 定義 Comment 結(jié)構(gòu)體及新建評(píng)論方法:
package?main
import?"errors"
type?Comment?struct?{
????Id?int
????Content?string
????Author?string
????Post?*Post
}
func?(comment?*Comment)?Create()?(err?error)?{
????if?comment.Post?==?nil?{
????????err?=?errors.New("Post?not?found")
????????return
????}
????sql?:=?"insert?into?comments?(content,?author,?post_id)?values?(?,??,??)"
????res,?err?:=?Db.Exec(sql,?comment.Content,?comment.Author,?comment.Post.Id)
????if?err?!=?nil?{
????????return
????}
????commentId,?_?:=?res.LastInsertId()
????comment.Id?=?int(commentId)
????return
}
在 Comment 中,可以通過(guò) Post *Post 指針引用其所屬的文章對(duì)象。
整體測(cè)試代碼
最后編寫(xiě) main.go 測(cè)試上述關(guān)聯(lián)查詢:
package?main
import?(
????"fmt"
)
func?main()??{
????//?插入文章記錄
????post?:=?Post{Title:?"Golang?數(shù)據(jù)庫(kù)編程",?Content:?"通過(guò)?go-sql-driver/mysql?包進(jìn)行表之間的關(guān)聯(lián)查詢",?Author:?"學(xué)院君"}
????post.Create()
????//?插入評(píng)論記錄
????comment1?:=?Comment{Content:?"測(cè)試評(píng)論1",?Author:?"學(xué)院君",?Post:?&post}
????comment1.Create()
????comment2?:=?Comment{Content:?"測(cè)試評(píng)論2",?Author:?"學(xué)院君",?Post:?&post}
????comment2.Create()
????//?查詢文章評(píng)論信息
????mysqlPost,?_?:=?GetPost(post.Id)
????fmt.Println(mysqlPost)
????fmt.Println(mysqlPost.Comments)
????fmt.Println(mysqlPost.Comments[0].Post)
}
我們?cè)?Post 和 Comment 結(jié)構(gòu)體中分別通過(guò) Comments 切片(數(shù)組指針)和 Post 指針定義兩者之間的一對(duì)多和多對(duì)一關(guān)聯(lián),然后在查詢文章記錄的 GetPost 方法中編寫(xiě)通過(guò) Post ID 查詢關(guān)聯(lián) Comment 記錄的代碼,在創(chuàng)建 Comment 的時(shí)候,也要確保對(duì)應(yīng)的 Post 字段不為空,即 post_id 字段不為空,這樣就將兩者通過(guò)代碼關(guān)聯(lián)起來(lái)了。
編譯 mysql 這個(gè)包,并運(yùn)行生成的二進(jìn)制可執(zhí)行程序,輸出結(jié)果如下:

表明關(guān)聯(lián)查詢成功。
雖然我們已經(jīng)構(gòu)建起關(guān)聯(lián)關(guān)系,但是全靠自己擼代碼有點(diǎn)麻煩,而且隨著應(yīng)用的增長(zhǎng),這種復(fù)雜度會(huì)越來(lái)越大。我們可以通過(guò) ORM 類來(lái)簡(jiǎn)化這個(gè)流程,目前 Go 語(yǔ)言中最流行的 ORM 實(shí)現(xiàn)非 GORM 莫屬,下篇教程,學(xué)院君就來(lái)給大家介紹 GORM 的基本使用。
(全文完)
推薦閱讀

