数据表之间关联关系和关联查询
关联关系简介
MySQL 之所以被称之为关系型数据库,是因为可以基于外键定义数据表之间的关联关系,日常开发常见的关联关系如下所示:
- 一对一:一张表的一条记录对应另一张表的一条记录,比如用户表与用户资料表
- 一对多:一张表的一条记录对应另一张表的多条记录,比如用户表与文章表、文章表与评论表
- 多对一:一张表的多条记录归属另一张表的一条记录(一对多的逆向操作)
- 多对多:一张表的多条记录归属另一张表的多条记录,此时仅仅基于两张表的字段已经无法定义这种关联关系,需要借助中间表来定义,比如文章表与标签表往往是这种关联
我们在上篇教程已经介绍了 Go 语言中基于第三方包 go-sql-driver/mysql 对单个数据表的增删改查操作,接下来我们来看看如何基于这个包对关联表进行操作。
新建评论表
为了方便演示,我们在 test_db
数据库中新建一张评论表 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;
这里我们创建了一个外键将 comments
表的 post_id
字段和 posts
表的 id
字段关联起来,并且通过 ON DELETE CASCADE
声明将两张表级联起来:当删除 posts
表中的某条记录时,自动删除 comments
中与之关联的评论记录(如果省略这个声明,则不能直接删除 posts
表中有 comments
关联依赖的记录)。
我们在 posts
和 comments
插入两条记录,这两条记录通过 comments.post_id
建立了外键关联:
此时,如果删除 posts
表中的记录,刷新 comments
表,会发现 comments
表对应记录也被清空,说明外键关联生效。
编写示例代码
接下来,我们编写一段示例代码演示如何在 Go 语言中通过 go-sql-driver/mysql
包对文章表和评论表进行关联查询。
新建一个 mysql
子目录来存放示例代码,这一次,我们通过拆分不同操作业务逻辑到不同文件来构建这个示例程序。
初始化连接
在 mysql
目录下新建一个 conn.go
编写数据库连接代码:
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
var Db *sql.DB
// 初始化数据库连接
func init() {
var err error
Db, err = sql.Open("mysql", "root:root@/test_db?charset=utf8mb4&parseTime=true")
if err != nil {
panic(err)
}
}
注意到 Db
变量首字母大写了,因此一旦初始化之后,就可以在当前包下的任何文件中直接引用了。
迁移文章增删改查代码
将 posts
表增删改查操作拆分到独立的 post.go
,并且在 Post
结构体中引入 Comments []Comment
属性存放关联的评论信息:
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)
// 查询与之关联的 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
}
我们在 GetPost
方法中获取单条文章记录后,再通过对应文章 ID 进行数据库查询获取相关评论信息存放到 post
对象的 Comments
属性中,这样就可以通过该属性获取文章的评论数据了。
定义评论相关操作
紧接着创建 comment.go
定义 Comment
结构体及新建评论方法:
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
中,可以通过 Post *Post
指针引用其所属的文章对象。
整体测试代码
最后编写 main.go
测试上述关联查询:
package main
import (
"fmt"
)
func main() {
// 插入文章记录
post := Post{Title: "Golang 数据库编程", Content: "通过 go-sql-driver/mysql 包进行表之间的关联查询", Author: "学院君"}
post.Create()
// 插入评论记录
comment1 := Comment{Content: "测试评论1", Author: "学院君", Post: &post}
comment1.Create()
comment2 := Comment{Content: "测试评论2", Author: "学院君", Post: &post}
comment2.Create()
// 查询文章评论信息
mysqlPost, _ := GetPost(post.Id)
fmt.Println(mysqlPost)
fmt.Println(mysqlPost.Comments)
fmt.Println(mysqlPost.Comments[0].Post)
}
我们在 Post
和 Comment
结构体中分别通过 Comments
切片(数组指针)和 Post
指针定义两者之间的一对多和多对一关联,然后在查询文章记录的 GetPost
方法中编写通过 Post ID 查询关联 Comment 记录的代码,在创建 Comment 的时候,也要确保对应的 Post 字段不为空,即 post_id
字段不为空,这样就将两者通过代码关联起来了。
编译 mysql
这个包,并运行生成的二进制可执行程序,输出结果如下:
表明关联查询成功。
虽然我们已经构建起关联关系,但是全靠自己撸代码有点麻烦,而且随着应用的增长,这种复杂度会越来越大。我们可以通过 ORM 类来简化这个流程,目前 Go 语言中最流行的 ORM 实现非 GORM 莫属,下篇教程,学院君就来给大家介绍 GORM 的基本使用。
No Comments