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.
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 member
và club
là M-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
, Club
và MemberClub
.
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
, club
và member_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
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
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
Bình luận