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.

1-m

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 foobar và chèn 1 số dữ liệu cho 2 bảng này. Mỗi quan hệ giữa foobar1-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à FooBar. 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à foobar. 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

kết quả 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

kết quả 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

kết quả 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

Kết quả 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

bảng foo


Kiểm tra trong bảng bar

bảng bar