Quan hệ 1-m là loại quan hệ rất phổ biến khi chúng ta thiết kế bảng. Trong mối quan hệ này, 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, nhưng một bản ghi trong bảng thứ hai chỉ có một 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ệ 1-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 và chèn dữ liệu mẫu
Để 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.foo (
id varchar(10) PRIMARY KEY,
name varchar(50) NOT NULL
);
CREATE TABLE db.bar (
id varchar(10) PRIMARY KEY,
name varchar(50) NOT null,
foo_id varchar(10) REFERENCES db.foo(id) ON DELETE CASCADE
);
INSERT INTO db.foo (id, name) VALUES
('ox-01', 'foo1'),
('ox-02', 'foo2')
INSERT INTO db.bar (id, name, foo_id) VALUES
('bar1', 'this is bar 1', 'ox-01'),
('bar2', 'this is bar 2', 'ox-02'),
('bar3', 'this is bar 3', 'ox-01'),
('bar4', 'this is bar 4', 'ox-01')
Ở đây chúng ta sẽ tạo 2 bảng foo
và bar
và chèn 1 số dữ liệu cho 2 bảng này. Mỗi quan hệ giữa foo
và bar
là 1-M, tức là một bản ghi foo
có thể có 0 hoặc nhiều bản ghi bar
Định nghĩa model
// file model/model.go
package model
type Foo struct {
Id string `gorm:"primaryKey"`
Name string
Bars []Bar `gorm:"foreignKey:FooId"`
}
type Bar struct {
Id string `gorm:"primaryKey"`
Name string
FooId string `gorm:"column:foo_id"`
Foo Foo
}
Chúng ta định nghĩa 2 struct model là Foo
và Bar
. Mặc định thì Id của struct sẽ tham chiếu đến primary key trong bảng (vì vậy có thể bỏ qua tag gorm cho thuộc tính trong struct)
Mặc định tên bảng sẽ được tham chiếu từ tên của struct. Ví dụ struct Foo
-> tên bảng foos
, Bar
-> bar
, nhưng ở phần tạo bảng bên trên chúng ta lại tạo 2 bảng là foo
và bar
. Nên để struct tham chiếu đến đúng bảng, lúc này chúng ta cần overide tableName
func(f *Foo) TableName() string{
return "foo"
}
func(b *Bar) TableName() string{
return "bar"
}
Chúng ta có thể thay đổi tên bảng default bằng cách implement interface Tabler
type Tabler interface {
TableName() string
}
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 chúng ta khởi tạo database bằng container
Viết repo thao tác với CSDL + Viết unit test
1.Lấy danh sách Foo - Bar
// File repo/query.go
func GetFooBar() (foos []model.Foo, err error) {
if err := DB.Preload("Bars").Find(&foos).Error; err != nil {
return nil, err
}
return foos, nil
}
Viết unit test
// file test/repo_test.go
package test
import (
"fmt"
"one-many/repo"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_GetFooBar(t *testing.T) {
foos, err := repo.GetFooBar()
assert.Nil(t, err)
for _, foo := range foos {
fmt.Println(foo.Name)
for _, bar := range foo.Bars {
fmt.Println(" " + bar.Name)
}
}
assert.Positive(t, len(foos))
}
Kết quả khi run test
2.Lấy thông tin của Foo theo ID
// File repo/query.go
func GetFooById(id string) (foo model.Foo, err error) {
if err := DB.Preload("Bars").Find(&foo, "foo.id = ?", id).Error; err != nil {
return model.Foo{}, err
}
return foo, nil
}
Viết unit test
// file test/repo_test.go
func Test_GetFooById(t *testing.T) {
foo, err := repo.GetFooById("ox-01")
assert.Nil(t, err)
fmt.Println(foo.Name)
for _, bar := range foo.Bars {
fmt.Println(" " + bar.Name)
}
}
Kết quả khi run test
3.Lấy thông tin của Bar theo ID
// File repo/query.go
func GetBarById(id string) (bar model.Bar, err error) {
bar = model.Bar{
Id: id,
}
if err = DB.Preload("Foo").Find(&bar).Error; err != nil {
return model.Bar{}, err
}
return bar, nil
}
Viết unit test
// file test/repo_test.go
func Test_GetBarById(t *testing.T) {
bar, err := repo.GetBarById("bar1")
assert.Nil(t, err)
fmt.Println(bar.FooId)
fmt.Println(" " + bar.Id)
fmt.Println(" " + bar.Name)
}
Kết quả khi run test
4.Mockup Data
Ở bước đầu tiên chúng ta tạo bảng và insert bằng tay 1 vài bản ghi vào trong CSDL
Trong ví dụ này chúng ta sẽ thực hiện viết 1 function để insert tự động các bản ghi vào trong CSDL
func CreateData() (err error) {
// Tạo mảng foo để lưu kết quả
var foos []model.Foo
// Sử dụng vòng lặp để tạo 1 số đối tượng foo
for i := 0; i < 5; i++ {
foo_id := NewID() // random fooID
foo := model.Foo{
Id: foo_id,
Name: gofakeit.Animal(), // Fake name
}
// Với mỗi đối tượng foo -> tạo 1 số đối tượng bar tương ứng
for j := 0; j < 2+random.Intn(2); j++ {
bar := model.Bar{
Id: NewID(),
Name: gofakeit.Animal(),
FooId: foo_id,
}
foo.Bars = append(foo.Bars, bar)
}
foos = append(foos, foo)
}
// Insert vào trong CSDL
if err := DB.Create(&foos).Error; err != nil {
return err
}
return nil
}
Để random ID cho các đối tượng foo, bar chúng ta định nghĩa thêm func NewID()
// file repo/util.go
package repo
import gonanoid "github.com/matoous/go-nanoid/v2"
func NewID() (id string) {
id, _ = gonanoid.New(8)
return
}
Viết unit test
func Test_CreateData(t *testing.T) {
err := repo.CreateData()
assert.Nil(t, err)
}
Kết quả khi test
Để kiểm tra lại xem dữ liệu đã được insert hay chưa, chúng ta có thể kiểm tra trực tiếp trong database hoặc run test function Test_GetFooBar
Kiểm tra trong bảng foo
Kiểm tra trong bảng bar
Bình luận