ORM là tên viết tắt của cụm từ “ Object Relational Mapping” đây là tên gọi chỉ việc ánh xạ các record dữ liệu trong hệ quản trị cơ sở dữ liệu sang các cấu trúc dữ liệu (đối tượng) trong các ngôn ngữ lập trình

GORM là một thư viện ORM dành riêng cho Golang. Trong bài viết này chúng ta sẽ cùng tìm hiểu về thư viện tuyệt vời này

Chi tiết về thư viện GORM, các bạn có thể tham khảo tại link sau : https://gorm.io/docs/index.html

Tạo cơ sở dữ liệu mysql

Trong bài viết này chúng ta sẽ sử dụng GORM để kết nối với hệ CSDL mysql, vì vậy chúng ta cần cài đặt mysql trước

Chúng ta có thể cài trực tiếp hệ cơ sở dữ liệu mysql trên máy hoặc sử dụng docker để cài đặt. Ở bài viết này thì mình sẽ cài mysql sử dụng docker

Chúng ta có thể cài mysql thông qua docker compose hoặc docker run

1.Sử dụng docker compose

Tạo file docker-compose.yml

volumes:
  data_db:

services:
  mysql:
    image: mysql:latest
    volumes:
      - data_db:/var/lib/mysql
    ports:
      - 3306:3306
    environment:
      MYSQL_PASSWORD: 123
      MYSQL_ROOT_PASSWORD: 123
      MYSQL_DATABASE: db

Sau đó chạy file docker compose để khởi tạo container

docker-compose up -d

2.Sử dụng docker run

docker run -d --name db_mysql -p 3306:3306 \
-e MYSQL_PASSWORD=123 \
-e MYSQL_ROOT_PASSWORD=123 \
-e MYSQL_DATABASE=db \
mysql:latest

Trường hợp máy bạn sử dụng là laptop M1 không thể pull image mysql thì xem hướng dẫn tại đây : https://stackoverflow.com/questions/65456814/docker-apple-silicon-m1-preview-mysql-no-matching-manifest-for-linux-arm64-v8

Kết nối với CSDL sử dụng DBeaver

DBeaver là ứng dụng UI giúp chúng ta kết nối và thao tác với các hệ CSDL : Postgresql, mysql, DB2, Oracle, ...

Kết nối với CSDL sử dụng DBeaver

Trường hợp bị lỗi public key hãy tham khảo link sau : https://community.atlassian.com/t5/Confluence-questions/MySQL-Public-Key-Retrieval-is-not-allowed/qaq-p/778956

Nếu thành công, chúng ta sẽ có giao diện về mysql như sau

Giao diện DBeaver kết nối với mysql

Bây giờ chúng ta sẽ tạo bảng student và insert một số bản ghi cho bảng

CREATE TABLE db.student (
  id varchar(10) NOT NULL,
  full_name varchar(50),
  email varchar(50),
  phone varchar(50),
  card_id varchar(50),
  PRIMARY KEY (id)
)

INSERT INTO db.student(id,full_name,email,phone,card_id) VALUES
('1','anna','anna@gmail.com','0987654321','100'),
('2','bob','bob@gmail.com','0344005934','101'),
('3','john','john@gmail.com','0964543470','102'),
('4','alice','alice@gmail.com','0987123456','103')

Golang - GORM - Mysql

Kết nối

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

package main

import (
    "fmt"

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

// Định nghĩa struct Student ánh xạ sang bảng student trong database
type Student struct {
    Id       string `gorm:"primary_key"` // Id là trường mặc định được sử dụng làm khóa chính
    FullName string
    Email    string
    Phone    string
    CardId   string
}

// Khai báo các thông số kết nối database
var (
    host     string = "localhost"
    port     string = "3306"
    username string = "root"
    password string = "123"
    database string = "db"
)

var DB *gorm.DB

func main() {
    // Tạo connection string với các thông số đã được định nghĩa ở trên
    connectString := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        username,
        password,
        host,
        port,
        database,
    )

    var err error
    // Kết nối tới database dựa vào connection string
    DB, err = gorm.Open(mysql.Open(connectString), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info), // -> Log các câu lệnh truy vấn database trong terminal
    })

    // Dừng chương trình nếu quá trình kết nối tới database xảy ra lỗi
    if err != nil {
        panic("Failed to connect database")
    }
}

Table

GORM sẽ tự động chuyển tên struct thành tên bảng theo dạng là snake_case

Ví dụ : Student -> students, User -> users, UserDetail -> user_details

type Student struct {
  Id       string `gorm:"primary_key"` // Id là trường mặc định được sử dụng làm khóa chính
  FullName string
  Email    string
  Phone    string
  CardId   string
}

Chúng ta có thể thay đổi tên bảng default bằng cách implement interface Tabler

type Tabler interface {
  TableName() string
}

Vì ở bên trên chúng ta đã tạo bảng student, nên lúc này cần overide tên bảng

func(s *Student) TableName() string{
  return "student"
}

Create Object

// Khởi tạo transaction
tx := DB.Begin()
if err := tx.Error; err != nil {
    return
}

// Tạo đối tượng dựa trên model
std := model.Student{
    Id:       NewID(),
    FullName: gofakeit.Animal(),
    Email:    gofakeit.Email(),
    Phone:    gofakeit.Phone(),
    CardId:   NewID(),
}
tx.Create(&std)

// Tạo đối tượng với các trường được chỉ định
tx.Select("Id", "FullName", "Email", "CardId").Create(&model.Student{
    Id:       NewID(),
    FullName: gofakeit.Animal(),
    Email:    gofakeit.Email(),
    Phone:    gofakeit.Phone(),
    CardId:   NewID(),
})

// Tạo đối tượng với các thông tin ngoại trừ các thông tin có trong Omit
tx.Omit("Email", "Phone", "CardId").Create(&model.Student{
    Id:       NewID(),
    FullName: gofakeit.Animal(),
    Email:    gofakeit.Email(),
    Phone:    gofakeit.Phone(),
    CardId:   NewID(),
})

// Tạo nhiều đối tượng cùng lúc
var students []model.Student
for i := 0; i < 5; i++ {
    newStudent := model.Student{
        Id:       NewID(),
        FullName: gofakeit.Animal(),
        Email:    gofakeit.Email(),
        Phone:    gofakeit.Phone(),
        CardId:   NewID(),
    }
    students = append(students, newStudent)
}

tx.Create(&students)

// Tạo đối tượng với struct
DB.Model(&model.Student{}).Create(map[string]interface{}{
    "Id":       NewID(),
    "FullName": gofakeit.Animal(),
    "Email":    gofakeit.Email(),
})

tx.Rollback()

Bên trên là một vài ví dụ về cách chúng ta insert một bản ghi vào database sử dụng GORM. Ở đây chúng ta bắt đầu với việc sử dụng transaction tx := DB.Begin() và kết thúc bằng tx.Rollback() để tránh các bản ghi được insert vào trong CSDL.

Trong này mình có bổ sung thêm function NewID để genarate tự động ID dựa trên package gonanoid. và package gofakeit để fake thông tin liên quan đến fullname, email, phone, ...

func NewID() (id string) {
    id, _ = gonanoid.New(8) // Random chuỗi ID với độ dài 8 lý tự
    return
}

Các bạn có thể cài bằng cách

// Cài đặt gonanoid
go get github.com/matoous/go-nanoid/v2

// Cài đặt gofakeit
go get github.com/brianvoe/gofakeit/v6

Select Object

var std model.Student

    //  Lấy bản ghi đầu tiên sau khi sắp xếp theo khóa chính
    DB.First(&std)

    //  Lấy bản ghi đầu tiên
    DB.Take(&std)

    //  Lấy bản ghi cuối cùng sau khi sắp xếp theo khóa chính
    DB.Last(&std)

    // Tìm kiếm theo khóa chính
    DB.First(&model.Student{}, "2")
    DB.First(&model.Student{}, "id = ?", "2")

    // Tìm kiếm theo danh sách khóa chính
    DB.Find(&[]model.Student{}, []string{"2", "3", "4"})

    // Lấy tất cả bản ghi trong bảng
    var students []model.Student
    DB.Find(&students)

    // Condition
    DB.Where("full_name = ?", "bob").First(&model.Student{})

Hook (Object Life Cycle)

Hook là các hàm được gọi trước hoặc sau khi thực hiện các thao tác creation, querying, updating, deletion

Chúng ta có thể chỉ định hook cho một model (struct) cụ thể nào đó, nó sẽ tự động được gọi khi creation, querying, updating, deletion

Nếu khi hook được gọi mà xảy ra lỗi, lúc này GORM sẽ rollback transaction

Hook tạo object

Khi tạo object chúng ta có 4 Hook sau

BeforeSave
BeforeCreate
AfterCreate
AfterSave

Demo Hook BeforeCreate

Trước khi tạo đối tượng, nếu đối tượng chưa có ID thì tự động tạo ID

func (s *Student) BeforeCreate(tx *gorm.DB) (err error) {
  if s.Id == "" {
    fmt.Println("Bổ sung Id cho student")
    s.Id = util.NewID()
  }
  return
}

Tương tự chúng có các Hook khi thực hiện update, delete, find

Hook Update

BeforeSave
BeforeUpdate
AfterUpdate
AfterSave

Hook Delete

BeforeDelete
AfterDelete

Ví dụ: Nếu student có id = "1" thì không được phép xóa

func (s *Student) BeforeDelete(tx *gorm.DB) (err error) {
    if s.Id == "1" {
        return errors.New("user not allowed to delete")
    }
    return
}

Hook Query

AfterFind

Ví dụ: In ra thông tin của student mỗi khi tìm kiếm

func(s *Student) ToString() {
  fmt.Println("Id : ", s.Id)
  fmt.Println("FullName : ", s.FullName)
  fmt.Println("Email : ", s.Email)
  fmt.Println("Phone : ", s.Phone)
  fmt.Println("CardId : ", s.CardId)
}

func (s *Student) AfterFind(tx *gorm.DB) (err error) {
  s.ToString()
  return
}