Quan hệ M-M là gì?

Chúng ta có thể hiểu như sau: một bản ghi trong bảng thứ nhất có thể có nhiều bản ghi phù hợp trong bảng thứ 2, và ngược lại một bản ghi trong bảng thứ hai có thể có nhiều bản ghi phù hợp trong bảng thứ nhất.

M-M

Trong bài viết này, chúng ta cùng tìm hiểu về mối quan hệ M-M giữa các đối tượng bằng việc sử dụng Golang kết nối với cơ sở dữ liệu Mysql thông qua GORM

Cấu trúc thư mục

.
├── ddl -> chứa các câu lệnh DDL để tạo bảng
├── go.mod
├── go.sum
├── model -> định nghĩa model - entity
├── repo -> Chứa hàm thao tác với cơ sở dữ liệu
└── test -> Viết các hàm unit test để kiểm tra repo

Tạo bảng trong database

Để tạo bảng chúng ta sử dụng DBeaver và kết nối với container chạy database mysql (nếu chưa làm được phần này hãy xem lại bài viết Tìm hiểu về GORM trong Golang (phần 1) và làm theo hướng dẫn)

Câu lệnh DDL tạo bảng

// file DDL/ddl.sql

CREATE TABLE db.member (
  id VARCHAR(10) PRIMARY KEY,
  name VARCHAR(30) NOT NULL
);

CREATE TABLE db.club (
  id VARCHAR(10) PRIMARY KEY,
  name VARCHAR(30) NOT NULL
);

CREATE TABLE db.member_club (
  member_id VARCHAR(10) REFERENCES db.member(id) ON DELETE cascade,
  club_id VARCHAR(10) REFERENCES db.club(id) ON DELETE cascade,
  active BOOLEAN NOT NULL
);

Ở đây chúng ta sẽ tạo 3 bảng member, club, member_club. Mỗi quan hệ giữa memberclubM-M, và bảng member_club là bảng trung gian thể hiện cho chúng ta biết về mỗi quan hệ này

Định nghĩa model

// file model/model.go

package model

//----- Quan hệ Nhiều - Nhiều
type Member struct {
    Id    string `gorm:"primaryKey"`
    Name  string
    Clubs []Club `gorm:"many2many:member_club;"` // thể hiện mối quan hệ nhiều nhiều với bảng member_club
}

type Club struct {
    Id      string `gorm:"primaryKey"`
    Name    string
    Members []Member `gorm:"many2many:member_club;"` // thể hiện mối quan hệ nhiều nhiều với bảng member_club
}

type MemberClub struct {
    MemberId string `gorm:"primaryKey" column:"member_id"`
    ClubId   string `gorm:"primaryKey" column:"club_id"`
    Active   bool
}

Chúng ta định nghĩa 3 struct model là Member, ClubMemberClub.

Override tableName để struct có thể tham chiếu đến tên bảng trong database mà chúng ta đã tạo trước đó

func (m *Member) TableName() string {
    return "member"
}

func (c *Club) TableName() string {
    return "club"
}

func (mc *MemberClub) TableName() string {
    return "member_club"
}

Trong phần tạo bảng trong database, chúng ta tạo 3 bảng là member, clubmember_club. Vì vậy cần Override tên bảng để các struct có thể ánh xạ đến các bảng tương ứng trong database

Kết nối với cơ sở dữ liệu

package repo

import (
    "fmt"
    "math/rand"
    "time"

    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

// Thông số kết nối đến CSDL
var (
    host     string = "localhost"
    port     string = "3306"
    username string = "root"
    password string = "123"
    database string = "db"
)

var (
    DB     *gorm.DB   // Kết nối đến CSDL
    random *rand.Rand // Đối tượng dùng để tạo random number
)

func init() {
    connectString := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        username,
        password,
        host,
        port,
        database,
    )

    // Kết nối với CSDL thông qua connection string
    var err error
    DB, err = gorm.Open(mysql.Open(connectString), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info), // Log câu lệnh sql trong console
    })

    // Xử lý nếu quá trình kết nối với CSDL bị lỗi
    if err != nil {
        panic("Failed to connect database")
    }

    //Khởi động engine sinh số ngẫu nhiên
    s1 := rand.NewSource(time.Now().UnixNano())
    random = rand.New(s1)
}

Chúng ta kết nối tới database thông qua connection string với các thông số khi khởi tạo database bằng container

Đồng thời có sử dụng config Logger để log các câu lệnh SQL trong terminal phục vụ cho việc quan sát và debug

Viết repo thao tác với CSDL + Viết unit test

1.Tạo dữ liệu cho bảng

// file repo/create.go
package repo

import (
    "many-to-many/model"

    gonanoid "github.com/matoous/go-nanoid/v2"
    "gorm.io/gorm"
)

// Random chuỗi ID sử dụng gonanoid
func NewID(length ...int) (id string) {
    id, _ = gonanoid.New(8)
    return
}

// Thực hiện mockup data cho các bảng
func AddMemberToClub() (err error) {
    // Khởi tạo transaction
    tx := DB.Begin()
    if err := tx.Error; err != nil {
        return err
    }

    //---- Tạo members
    john := model.Member{
        Id:   NewID(),
        Name: "John",
    }

    anna := model.Member{
        Id:   NewID(),
        Name: "Anna",
    }

    bob := model.Member{
        Id:   NewID(),
        Name: "Bob",
    }

    alice := model.Member{
        Id:   NewID(),
        Name: "Alice",
    }

    // Thêm các đối tượng vừa tạo vào mảng members
    var members []model.Member
    members = append(members, john, anna, bob, alice)

    // Insert các bản ghi member vào trong CSDL
    if err := tx.Create(&members).Error; err != nil {
        tx.Rollback()
        return err
    }

    //--- Club
    math := model.Club{
        Id:   NewID(),
        Name: "Math",
    }

    sport := model.Club{
        Id:   NewID(),
        Name: "Sport",
    }

    music := model.Club{
        Id:   NewID(),
        Name: "Music",
    }

    // Thêm các đối tượng vừa tạo vào mảng clubs
    var clubs []model.Club
    clubs = append(clubs, math, sport, music)

    // Insert các bản ghi club vào trong CSDL
    if err := tx.Create(&clubs).Error; err != nil {
        tx.Rollback()
        return err
    }

    //---- Thêm các thành viên vào club
    err = assignMembersToClub(tx, math, []model.Member{john, anna})
    if err != nil {
        tx.Rollback()
        return err
    }

    err = assignMembersToClub(tx, sport, []model.Member{bob, alice})
    if err != nil {
        tx.Rollback()
        return err
    }

    err = assignMembersToClub(tx, music, []model.Member{john, bob, alice})
    if err != nil {
        tx.Rollback()
        return err
    }

    return tx.Commit().Error
}

// Function thực hiện gắn các bản ghi member vào 1 club cụ thể
func assignMembersToClub(tx *gorm.DB, club model.Club, members []model.Member) (err error) {
    for _, member := range members {
        err := tx.Create(&model.MemberClub{
            MemberId: member.Id,
            ClubId:   club.Id,
            Active:   random.Intn(2) == 1, //random true or false
        }).Error
        if err != nil {
            return err
        }
    }
    return nil
}

Viết unit test để kiểm thử func AddMemberToClub

// file test/repo_test.go

package test

import (
    "many-to-many/repo"
    "testing"

    "github.com/stretchr/testify/assert"
)

func Test_AddMemberToClub(t *testing.T) {
    err := repo.AddMemberToClub()
    assert.Nil(t, err)
}

Thực hiện debug test

Thực hiện unit test

GORM đã thực hiện các câu lệnh query để insert các bản ghi vào trong CSDL

2.Tìm club theo tên, đồng thời lấy danh sách thành viên

// file repo/query.go

func GetClubByName(name string) (club model.Club, err error) {
    err = DB.Preload("Members").Find(&club, "name = ?", name).Error
    if err != nil {
        return model.Club{}, err
    }
    return club, nil
}

Viết unit test

// file test/repo_test.go

func Test_GetClubByName(t *testing.T) {
    club, err := repo.GetClubByName("Sport")
    assert.Nil(t, err)

    fmt.Println(club)

    fmt.Println(club.Name)
    for _, m := range club.Members {
        fmt.Println("    " + m.Name)
    }
}

Kết quả khi run test

Kết quả khi run test

3.Tìm thành viên, lấy danh sách club mà người đó tham gia

// File repo/query.go

func GetMemberByName(name string) (member model.Member, err error) {
    err = DB.Preload("Clubs").Find(&member, "name = ?", name).Error
    if err != nil {
        return model.Member{}, err
    }
    return member, nil
}

Viết unit test

// file test/repo_test.go

func Test_GetMemberByName(t *testing.T) {
    member, err := repo.GetMemberByName("Bob")
    assert.Nil(t, err)

    fmt.Println(member)

    fmt.Println(member.Name)
    for _, c := range member.Clubs {
        fmt.Println("    " + c.Name)
    }
}

Kết quả khi run test

Kết quả khi run test