数据表之间关联关系和关联查询


关联关系简介

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 关联依赖的记录)。

我们在 postscomments 插入两条记录,这两条记录通过 comments.post_id 建立了外键关联:

-w961

-w777

此时,如果删除 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)
}

我们在 PostComment 结构体中分别通过 Comments 切片(数组指针)和 Post 指针定义两者之间的一对多和多对一关联,然后在查询文章记录的 GetPost 方法中编写通过 Post ID 查询关联 Comment 记录的代码,在创建 Comment 的时候,也要确保对应的 Post 字段不为空,即 post_id 字段不为空,这样就将两者通过代码关联起来了。

编译 mysql 这个包,并运行生成的二进制可执行程序,输出结果如下:

-w1328

表明关联查询成功。

虽然我们已经构建起关联关系,但是全靠自己撸代码有点麻烦,而且随着应用的增长,这种复杂度会越来越大。我们可以通过 ORM 类来简化这个流程,目前 Go 语言中最流行的 ORM 实现非 GORM 莫属,下篇教程,学院君就来给大家介绍 GORM 的基本使用。


Vote Vote Cancel Collect Collect Cancel

<< 上一篇: 数据库连接建立和增删改查基本实现

>> 下一篇: GORM 使用入门