Skip to content

Gorm Thư viện ORM Database

Tài liệu chính thức: GORM - The fantastic ORM library for Golang, aims to be developer friendly.

Kho lưu trữ: go-gorm/gorm: The fantastic ORM library for Golang, aims to be developer friendly (github.com)

Trong cộng đồng Go, đối với phần tương tác database, có hai phe phái, một phe thích các thư viện đơn giản như sqlx, chức năng không mạnh mẽ nhưng có thể kiểm soát SQL mọi lúc, tối ưu hiệu suất đến mức tối đa. Phe còn lại thích ORM để phát triển hiệu quả, có thể tiết kiệm nhiều phiền phức không cần thiết trong quá trình phát triển. Khi nhắc đến ORM, trong cộng đồng Go không thể không nhắc đến gorm, đây là một ORM lâu đời, tương tự như xorm, ent và các ORM trẻ hơn khác. Bài viết này sẽ nói về nội dung liên quan đến gorm, bài viết chỉ giải thích các nội dung cơ bản, coi như là mở đầu, muốn tìm hiểu chi tiết sâu hơn có thể đọc tài liệu chính thức, tài liệu tiếng Trung của nó đã khá hoàn chỉnh, và tác giả cũng là một trong những người dịch tài liệu gorm.

Đặc điểm

  • ORM đầy đủ chức năng
  • Association (Has One, Has Many, Belongs To, Many To Many, Polymorphism, Single-table inheritance)
  • Hooks (Before/After Create/Save/Update/Delete/Find)
  • Preload với hỗ trợ Preload, Joins
  • Transaction, Nested Transaction, Save Point, Rollback To Saved Point
  • Context, Prepared Statement mode, DryRun mode
  • Batch Insert, FindInBatches, Find/Create with Map, CRUD với SQL expression, Context Valuer
  • SQL Builder, Upsert, Lock, Optimizer/Index/Comment Hint, Named Parameters, Subquery
  • Composite Primary Key, Index, Constraint
  • Auto Migration
  • Custom Logger
  • Flexible plugin API: Database Resolver (multiple databases, read/write splitting), Prometheus...
  • Mọi tính năng đều được kiểm thử kỹ lưỡng
  • Developer friendly

Gorm cũng có một số nhược điểm, ví dụ như hầu hết các tham số method đều là kiểu interface, nếu không đọc tài liệu thì khó mà biết được nên truyền gì, đôi khi có thể truyền struct, đôi khi có thể truyền string, đôi khi có thể truyền map, đôi khi có thể truyền slice, ngữ nghĩa khá mơ hồ, và nhiều trường hợp vẫn cần tự viết SQL.

Có hai ORM thay thế có thể thử, đầu tiên là aorm, mới开源不久, nó không cần tự viết tên trường của bảng, hầu hết là chain operation, dựa trên reflection, vì số sao không nhiều nên có thể chờ thêm. Thứ hai là ent, ORM do facebook开源, nó cũng hỗ trợ chain operation, và hầu hết không cần tự viết SQL,设计理念 dựa trên graph (graph trong cấu trúc dữ liệu), implementation dựa trên code generation thay vì reflection (khá đồng ý với điều này), nhưng tài liệu là tiếng Anh, có ngưỡng tiếp cận nhất định.

Cài đặt

Cài đặt thư viện gorm

sh
$ go get -u gorm.io/gorm

Kết nối

Hiện tại gorm hỗ trợ các database sau

  • MySQL: "gorm.io/driver/mysql"
  • PostgreSQL: "gorm.io/driver/postgres"
  • SQLite: "gorm.io/driver/sqlite"
  • SQL Server: "gorm.io/driver/sqlserver"
  • TIDB: "gorm.io/driver/mysql", TIDB tương thích giao thức mysql
  • ClickHouse: "gorm.io/driver/clickhouse"

Ngoài ra còn có một số driver database khác do các nhà phát triển bên thứ ba cung cấp, ví dụ như driver oracle CengSin/oracle. Bài viết này sẽ sử dụng MySQL để demo, sử dụng database nào thì cần cài đặt driver tương ứng, ở đây cài đặt driver Mysql của gorm.

sh
$ go get -u gorm.io/driver/mysql

Sau đó kết nối với database bằng dsn (data source name), driver library sẽ tự động parse dsn thành cấu hình tương ứng

go
package main

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
  "log/slog"
)

func main() {
  dsn := "root:123456@tcp(192.168.48.138:3306)/hello?charset=utf8mb4&parseTime=True&loc=Local"
  db, err := gorm.Open(mysql.Open(dsn))
  if err != nil {
    slog.Error("db connect error", err)
  }
  slog.Info("db connect success")
}

Hoặc truyền cấu hình thủ công

go
package main

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
  "log/slog"
)

func main() {
  db, err := gorm.Open(mysql.New(mysql.Config{}))
  if err != nil {
    slog.Error("db connect error", err)
  }
  slog.Info("db connect success")
}

Hai cách đều tương đương, tùy theo thói quen sử dụng.

Cấu hình kết nối

Bằng cách truyền cấu trúc gorm.Config, chúng ta có thể kiểm soát một số hành vi của gorm

go
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

Dưới đây là giải thích đơn giản, có thể cấu hình theo nhu cầu

go
type Config struct {
  // Disable default transaction, gorm sẽ开启 transaction khi create và update đơn để保持数据一致性
  SkipDefaultTransaction bool
  // Naming strategy tùy chỉnh
  NamingStrategy schema.Namer
  // Lưu association đầy đủ
  FullSaveAssociations bool
  // Logger tùy chỉnh
  Logger logger.Interface
  // NowFunc tùy chỉnh, dùng để inject trường CreatedAt và UpdatedAt
  NowFunc func() time.Time
  // Chỉ生成 SQL không thực thi
  DryRun bool
  // Sử dụng prepared statement
  PrepareStmt bool
  // Sau khi kết nối, ping database
  DisableAutomaticPing bool
  // Bỏ qua foreign key khi migrate database
  DisableForeignKeyConstraintWhenMigrating bool
  // Bỏ qua relationship reference khi migrate database
  IgnoreRelationshipsWhenMigrating bool
  // Disable nested transaction
  DisableNestedTransaction bool
  // Cho phép global update, tức là update không có where
  AllowGlobalUpdate bool
  // Query tất cả các trường của bảng
  QueryFields bool
  // Batch size cho batch insert
  CreateBatchSize int
  // Bật chuyển đổi lỗi
  TranslateError bool

  // ClauseBuilders clause builder
  ClauseBuilders map[string]clause.ClauseBuilder
  // ConnPool db conn pool
  ConnPool ConnPool
  // Dialector database dialector
  Dialector
  // Plugins registered plugins
  Plugins map[string]Plugin

  callbacks  *callbacks
  cacheStore *sync.Map
}

Model

Trong gorm, model tương ứng với bảng database, thường được thể hiện bằng struct, ví dụ như struct sau

go
type Person struct {
  Id      uint
  Name    string
  Address string
  Mom     string
  Dad     string
}

Struct内部 có thể包含 các kiểu dữ liệu cơ bản và các kiểu implements interface sql.Scannersql.Valuer. Mặc định, bảng được map từ struct Personpersons, theo phong cách snake case số nhiều. Tên cột cũng theo phong cách snake case, ví dụ Id tương ứng với cột id, gorm cũng cung cấp một số cách để cấu hình.

Chỉ định tên cột

Thông qua struct tag, chúng ta có thể chỉ định tên cột cho các trường struct, khi đó gorm sẽ sử dụng tên cột được chỉ định.

go
type Person struct {
  Id      uint   `gorm:"column:ID;"`
  Name    string `gorm:"column:Name;"`
  Address string
  Mom     string
  Dad     string
}

Chỉ định tên bảng

Bằng cách implements interface Table, có thể chỉ định tên bảng, nó chỉ có một method là trả về tên bảng.

go
type Tabler interface {
  TableName() string
}

Trong method implements, nó trả về string person, khi migrate database, gorm sẽ tạo bảng tên là person.

go
type Person struct {
  Id      uint   `gorm:"column:ID;"`
  Name    string `gorm:"column:Name;"`
  Address string
  Mom     string
  Dad     string
}

func (p Person) TableName() string {
  return "person"
}

Đối với naming strategy, cũng có thể truyền implements tùy chỉnh khi tạo kết nối để đạt được hiệu quả tùy chỉnh.

Time tracking

go
type Person struct {
  Id      uint
  Name    string
  Address string
  Mom     string
  Dad     string

  CreatedAt sql.NullTime
  UpdatedAt sql.NullTime
}

func (p Person) TableName() string {
  return "person"
}

Khi包含 các trường CreatedAt hoặc UpdatedAt, khi tạo hoặc cập nhật record, nếu giá trị bằng 0, gorm sẽ tự động sử dụng time.Now() để đặt thời gian.

go
db.Create(&Person{
    Name:    "jack",
    Address: "usa",
    Mom:     "lili",
    Dad:     "tom",
  })

// INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom','2023-10-25 14:43:57.16','2023-10-25 14:43:57.16')

Gorm cũng hỗ trợ tracking timestamp

go
type Person struct {
  Id      uint   `gorm:"primaryKey;"`
  Name    string `gorm:"primaryKey;"`
  Address string
  Mom     string
  Dad     string

  // nanoseconds
  CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
  // milliseconds
  UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}

Khi thực thi Create, tương đương với SQL sau

sql
INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom',1698216540519000000,1698216540)

Trong thực tế, nếu cần tracking thời gian, khuyến nghị lưu timestamp ở backend, xử lý đơn giản hơn trong trường hợp cross timezone.

Model

Gorm cung cấp struct Model preset, nó包含 ID primary key, và hai trường time tracking, và một trường soft delete.

go
type Model struct {
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt DeletedAt `gorm:"index"`
}

Khi sử dụng chỉ cần embed vào model entity của bạn

go
type Order struct {
  gorm.Model
  Name string
}

Như vậy nó sẽ tự động có tất cả các đặc tính của gorm.Model.

Primary Key

Mặc định, trường tên id là primary key, có thể chỉ định trường primary key bằng struct tag

go
type Person struct {
  Id      uint `gorm:"primaryKey;"`
  Name    string
  Address string
  Mom     string
  Dad     string

  CreatedAt sql.NullTime
  UpdatedAt sql.NullTime
}

Nhiều trường tạo thành composite primary key

go
type Person struct {
  Id      uint `gorm:"primaryKey;"`
  Name    string `gorm:"primaryKey;"`
  Address string
  Mom     string
  Dad     string

  CreatedAt sql.NullTime
  UpdatedAt sql.NullTime
}

Index

Có thể chỉ định column index thông qua struct tag index

go
type Person struct {
  Id      uint   `gorm:"primaryKey;"`
  Name    string `gorm:"primaryKey;"`
  Address string `gorm:"index:idx_addr,unique,sort:desc;"`
  Mom     string
  Dad     string

  // nanoseconds
  CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
  // milliseconds
  UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}

Trong struct trên, đã tạo unique index cho trường Address. Hai trường sử dụng cùng tên index sẽ tạo composite index

go
type Person struct {
    Id      uint   `gorm:"primaryKey;"`
    Name    string `gorm:"primaryKey;"`
    Address string `gorm:"index:idx_addr,unique;"`
    School  string `gorm:"index:idx_addr,unique;"`
    Mom     string
    Dad     string

    // nanoseconds
    CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
    // milliseconds
    UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}

Foreign Key

Trong struct, định nghĩa relationship foreign key được thực hiện bằng cách embed struct, ví dụ

go
type Person struct {
  Id   uint `gorm:"primaryKey;"`
  Name string

  MomId uint
  Mom   Mom `gorm:"foreignKey:MomId;"`

  DadId uint
  Dad   Dad `gorm:"foreignKey:DadId;"`
}

type Mom struct {
  Id   uint
  Name string

  Persons []Person `gorm:"foreignKey:MomId;"`
}

type Dad struct {
  Id   uint
  Name string

  Persons []Person `gorm:"foreignKey:DadId;"`
}

Trong ví dụ, struct Person có hai foreign key, lần lượt reference primary key của hai struct DadMom, mặc định là reference primary key. Person đối với DadMom là relationship one-to-one, một người chỉ có thể có một cha và mẹ. DadMom đối với Person là relationship one-to-many, vì cha và mẹ có thể có nhiều con.

go
Mom   Mom `gorm:"foreignKey:MomId;"`

Tác dụng của embed struct là để chỉ định foreign key và reference dễ dàng, mặc định tên trường foreign key theo format TênKiểuReference+Id, ví dụ MomId. Mặc định là reference primary key, có thể chỉ định reference trường nào đó thông qua struct tag

go
type Person struct {
  Id   uint `gorm:"primaryKey;"`
  Name string

  MomId uint
  Mom   Mom `gorm:"foreignKey:MomId;references:Sid;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`

  DadId uint
  Dad   Dad `gorm:"foreignKey:DadId;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

type Mom struct {
  Id   uint
  Sid  uint `gorm:"uniqueIndex;"`
  Name string

  Persons []Person `gorm:"foreignKey:MomId;"`
}

Trong đó constraint:OnUpdate:CASCADE,OnDelete:SET NULL; là định nghĩa foreign key constraint.

Hooks

Một model entity có thể自定义 hooks

  • Create
  • Update
  • Delete
  • Query

Các interface tương ứng như sau

go
// Trigger trước khi tạo
type BeforeCreateInterface interface {
    BeforeCreate(*gorm.DB) error
}

// Trigger sau khi tạo
type AfterCreateInterface interface {
    AfterCreate(*gorm.DB) error
}

// Trigger trước khi cập nhật
type BeforeUpdateInterface interface {
    BeforeUpdate(*gorm.DB) error
}

// Trigger sau khi cập nhật
type AfterUpdateInterface interface {
    AfterUpdate(*gorm.DB) error
}

// Trigger trước khi lưu
type BeforeSaveInterface interface {
    BeforeSave(*gorm.DB) error
}

// Trigger sau khi lưu
type AfterSaveInterface interface {
    AfterSave(*gorm.DB) error
}

// Trigger trước khi xóa
type BeforeDeleteInterface interface {
    BeforeDelete(*gorm.DB) error
}

// Trigger sau khi xóa
type AfterDeleteInterface interface {
    AfterDelete(*gorm.DB) error
}

// Trigger sau khi query
type AfterFindInterface interface {
    AfterFind(*gorm.DB) error
}

Struct có thể自定义 một số hành vi bằng cách implements các interface này.

Tags

Dưới đây là một số tags mà gorm hỗ trợ

TagMô tả
columnChỉ định tên cột db
typeKiểu dữ liệu cột, khuyến nghị sử dụng kiểu通用 tương thích tốt, ví dụ: bool, int, uint, float, string, time, bytes đều được hỗ trợ bởi tất cả database và có thể sử dụng cùng với các tags khác, ví dụ: not null, size, autoIncrement... Cũng hỗ trợ chỉ định kiểu dữ liệu database như varbinary(8). Khi sử dụng kiểu dữ liệu database chỉ định, cần là kiểu dữ liệu database đầy đủ, như: MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializerChỉ định serializer để serialize hoặc deserialize dữ liệu vào database, ví dụ: serializer:json/gob/unixtime
sizeĐịnh nghĩa kích thước hoặc độ dài của kiểu dữ liệu cột, ví dụ size: 256
primaryKeyĐịnh nghĩa cột là primary key
uniqueĐịnh nghĩa cột là unique key
defaultĐịnh nghĩa giá trị mặc định của cột
precisionChỉ định precision của cột
scaleChỉ định scale của cột
not nullChỉ định cột là NOT NULL
autoIncrementChỉ định cột là auto increment
autoIncrementIncrementAuto increment increment, kiểm soát khoảng cách giữa các record liên tiếp
embeddedEmbedded field
embeddedPrefixPrefix tên cột cho embedded field
autoCreateTimeTrack thời gian hiện tại khi tạo, đối với trường int, nó sẽ track timestamp giây, có thể sử dụng nano/milli để track timestamp nanosecond, millisecond, ví dụ: autoCreateTime:nano
autoUpdateTimeTrack thời gian hiện tại khi tạo/cập nhật, đối với trường int, nó sẽ track timestamp giây, có thể sử dụng nano/milli để track timestamp nanosecond, millisecond, ví dụ: autoUpdateTime:milli
indexTạo index theo tham số, nhiều trường sử dụng cùng tên sẽ tạo composite index, xem Indexes để biết chi tiết
uniqueIndexGiống index nhưng tạo unique index
checkTạo check constraint, ví dụ check:age > 13, xem Constraints để biết chi tiết
<-Đặt quyền ghi của trường, <-:create chỉ tạo, <-:update chỉ cập nhật, <-:false không có quyền ghi, <- quyền tạo và cập nhật
->Đặt quyền đọc của trường, ->:false không có quyền đọc
-Bỏ qua trường, -表示 không đọc ghi, -:migration表示 không có quyền migration, -:all表示 không có quyền đọc ghi migration
commentThêm comment cho trường khi migration
foreignKeyChỉ định cột của model hiện tại làm foreign key của join table
referencesChỉ định tên cột của reference table, sẽ được map thành foreign key của join table
polymorphicChỉ định polymorphic type, ví dụ tên model
polymorphicValueChỉ định polymorphic value, mặc định là tên bảng
many2manyChỉ định tên join table
joinForeignKeyChỉ định tên foreign key column của join table, sẽ được map到 current table
joinReferencesChỉ định tên foreign key column của join table, sẽ được map到 reference table
constraintRelationship constraints, ví dụ: OnUpdate, OnDelete

Migration

Method AutoMigrate sẽ giúp chúng ta tự động migrate, nó sẽ tạo bảng, constraints, indexes, foreign keys, v.v.

go
func (db *DB) AutoMigrate(dst ...interface{}) error

Ví dụ

go
type Person struct {
  Id      uint   `gorm:"primaryKey;"`
  Name    string `gorm:"type:varchar(100);uniqueIndex;"`
  Address string
}

type Order struct {
  Id   uint
  Name string
}

db.AutoMigrate(Person{}, Order{})
// CREATE TABLE `person` (`id` bigint unsigned AUTO_INCREMENT,`name` varchar(100),`address` longtext,PRIMARY KEY (`id`),UNIQUE INDEX `idx_person_name` (`name`))
// CREATE TABLE `orders` (`id` bigint unsigned AUTO_INCREMENT,`name` longtext,PRIMARY KEY (`id`))

Hoặc có thể thao tác thủ công, truy cập interface Migrator thông qua method Migrator

go
func (db *DB) Migrator() Migrator

Nó hỗ trợ các method interface sau

go
type Migrator interface {
  // AutoMigrate
  AutoMigrate(dst ...interface{}) error

  // Database
  CurrentDatabase() string
  FullDataTypeOf(*schema.Field) clause.Expr
  GetTypeAliases(databaseTypeName string) []string

  // Tables
  CreateTable(dst ...interface{}) error
  DropTable(dst ...interface{}) error
  HasTable(dst interface{}) bool
  RenameTable(oldName, newName interface{}) error
  GetTables() (tableList []string, err error)
  TableType(dst interface{}) (TableType, error)

  // Columns
  AddColumn(dst interface{}, field string) error
  DropColumn(dst interface{}, field string) error
  AlterColumn(dst interface{}, field string) error
  MigrateColumn(dst interface{}, field *schema.Field, columnType ColumnType) error
  HasColumn(dst interface{}, field string) bool
  RenameColumn(dst interface{}, oldName, field string) error
  ColumnTypes(dst interface{}) ([]ColumnType, error)

  // Views
  CreateView(name string, option ViewOption) error
  DropView(name string) error

  // Constraints
  CreateConstraint(dst interface{}, name string) error
  DropConstraint(dst interface{}, name string) error
  HasConstraint(dst interface{}, name string) bool

  // Indexes
  CreateIndex(dst interface{}, name string) error
  DropIndex(dst interface{}, name string) error
  HasIndex(dst interface{}, name string) bool
  RenameIndex(dst interface{}, oldName, newName string) error
  GetIndexes(dst interface{}) ([]Index, error)
}

Danh sách method liên quan đến nhiều khía cạnh database, table, column, view, index, constraint, cho phép người dùng cần tùy chỉnh có thể thao tác chi tiết hơn.

Chỉ định table comment

Khi migrate, nếu muốn thêm table comment, có thể thiết lập như sau

go
db.Set("gorm:table_options", " comment 'person table'").Migrator().CreateTable(Person{})

Cần lưu ý nếu sử dụng method AutoMigrate() để migrate, và các struct có relationship reference, gorm sẽ recursive tạo bảng reference trước, điều này dẫn đến table comment của bảng được reference và bảng reference đều bị lặp, vì vậy khuyến nghị sử dụng method CreateTable để tạo.

TIP

Khi tạo bảng, method CreateTable cần đảm bảo bảng được reference được tạo trước bảng reference, nếu không sẽ báo lỗi, trong khi method AutoMigrate thì không cần, vì nó sẽ recursive tạo theo relationship reference.

Create

Create

Khi tạo record mới, hầu hết sẽ sử dụng method Create

go
func (db *DB) Create(value interface{}) (tx *DB)

Hiện có struct sau

go
type Person struct {
  Id   uint `gorm:"primaryKey;"`
  Name string
}

Tạo một record

go
user := Person{
    Name: "jack",
}

// Phải truyền reference
db = db.Create(&user)

// Lỗi xảy ra trong quá trình thực thi
err = db.Error
// Số lượng tạo
affected := db.RowsAffected

Sau khi tạo xong, gorm sẽ ghi primary key vào struct user, vì vậy phải truyền pointer. Nếu truyền một slice, sẽ tạo batch

go
user := []Person{
    {Name: "jack"},
    {Name: "mike"},
    {Name: "lili"},
}

db = db.Create(&user)

Tương tự, gorm cũng sẽ ghi primary key vào slice. Khi dữ liệu quá lớn, cũng có thể sử dụng method CreateInBatches để tạo theo batch, vì SQL生成 INSERT INTO table VALUES (),() sẽ rất dài, mỗi database có giới hạn độ dài SQL, vì vậy cần thiết có thể chọn tạo theo batch.

go
db = db.CreateInBatches(&user, 50)

Ngoài ra, method Save cũng có thể tạo record, tác dụng của nó là khi primary key khớp thì cập nhật record, ngược lại thì insert.

go
func (db *DB) Save(value interface{}) (tx *DB)
go
user := []Person{
    {Name: "jack"},
    {Name: "mike"},
    {Name: "lili"},
}

db = db.Save(&user)

Upsert

Method Save chỉ có thể khớp primary key, chúng ta có thể xây dựng Clause để hoàn thành upsert tùy chỉnh hơn. Ví dụ dòng code sau

go
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    DoNothing: false,
    DoUpdates: clause.AssignmentColumns([]string{"address"}),
    UpdateAll: false,
}).Create(&p)

Tác dụng của nó là khi trường name conflict, cập nhật giá trị của trường address, không conflict thì tạo record mới. Cũng có thể không làm gì khi conflict

go
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    DoNothing: true,
}).Create(&p)

Hoặc trực tiếp cập nhật tất cả các trường

go
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    UpdateAll: true,
}).Create(&p)

Trước khi sử dụng upsert, nhớ thêm index cho các trường conflict.

Query

First

Gorm cung cấp khá nhiều method cho query, đầu tiên là method First

go
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB)

Tác dụng của nó là tìm record đầu tiên theo primary key ascending, ví dụ

go
var person Person
result := db.First(&person)
err := result.Error
affected := result.RowsAffected

Truyền pointer dest để gorm map dữ liệu query được vào struct.

Hoặc sử dụng method TableModel để chỉ định bảng query, cái trước nhận string table name, cái sau nhận entity model.

db.Table("person").Find(&p)
db.Model(Person{}).Find(&p)

TIP

Nếu pointer truyền vào包含 entity model ví dụ như struct pointer, hoặc struct slice pointer, thì không cần chỉ định bảng nào để query, quy tắc này áp dụng cho tất cả các thao tác增删改查.

Take

Method Take tương tự First, khác biệt là không sort theo primary key.

go
func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB)
go
var person Person
result := db.Take(&person)
err := result.Error
affected := result.RowsAffected

Pluck

Method Pluck dùng để query batch một column của bảng, kết quả query có thể collect vào slice của kiểu chỉ định, không nhất thiết phải là slice của entity type.

go
func (db *DB) Pluck(column string, dest interface{}) (tx *DB)

Ví dụ collect tất cả address của mọi người vào string slice

go
var adds []string

// SELECT `address` FROM `person` WHERE name IN ('jack','lili')
db.Model(Person{}).Where("name IN ?", []string{"jack", "lili"}).Pluck("address", &adds)

Thực tế tương đương với

go
db.Select("address").Where("name IN ?", []string{"jack", "lili"}).Find(&adds)

Count

Method Count dùng để thống kê số lượng entity record

go
func (db *DB) Count(count *int64) (tx *DB)

Xem ví dụ sử dụng

go
var count int64

// SELECT count(*) FROM `person`
db.Model(Person{}).Count(&count)

Find

Method thường dùng nhất cho query batch là Find

go
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB)

Nó sẽ tìm tất cả các record phù hợp theo điều kiện đã cho

go
// SELECT * FROM `person`
var ps []Person
db.Find(&ps)

Select

Gorm mặc định query tất cả các trường, có thể chỉ định trường thông qua method Select

go
func (db *DB) Select(query interface{}, args ...interface{}) (tx *DB)

Ví dụ

go
// SELECT `address`,`name` FROM `person` ORDER BY `person`.`id` LIMIT 1
db.Select("address", "name").First(&p)

Tương đương với

go
db.Select([]string{"address", "name"}).First(&p)

Đồng thời, có thể sử dụng method Omit để bỏ qua các trường

go
func (db *DB) Omit(columns ...string) (tx *DB)

Ví dụ

go
// SELECT `person`.`id`,`person`.`name` FROM `person` WHERE id IN (1,2,3,4)
db.Omit("address").Where("id IN ?", []int{1, 2, 3, 4}).Find(&ps)

Các trường được chọn hoặc bỏ qua bởi SelectOmit sẽ có tác dụng khi tạo query cập nhật.

Where

Query điều kiện sẽ sử dụng method Where

go
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB)

Dưới đây là ví dụ đơn giản

go
var p Person

db.Where("id = ?", 1).First(&p)

Sử dụng nhiều Where trong chain operation sẽ xây dựng nhiều statement AND, ví dụ

go
// SELECT * FROM `person` WHERE id = 1 AND name = 'jack' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).Where("name = ?", "jack").First(&p)

Hoặc sử dụng method Or để xây dựng statement OR

go
func (db *DB) Or(query interface{}, args ...interface{}) (tx *DB)
go
// SELECT * FROM `person` WHERE id = 1 OR name = 'jack' AND address = 'usa' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).
    Or("name = ?", "jack").
    Where("address = ?", "usa").
    First(&p)

Và method Not, đều tương tự

go
func (db *DB) Not(query interface{}, args ...interface{}) (tx *DB)
go
// SELECT * FROM `person` WHERE id = 1 OR name = 'jack' AND NOT name = 'mike' AND address = 'usa' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).
    Or("name = ?", "jack").
    Not("name = ?", "mike").
    Where("address = ?", "usa").
    First(&p)

Đối với điều kiện IN, có thể truyền slice trực tiếp trong method Where.

go
db.Where("address IN ?", []string{"cn", "us"}).Find(&ps)

Hoặc điều kiện IN nhiều column, cần sử dụng kiểu [][]any để chứa tham số

go
// SELECT * FROM `person` WHERE (id, name, address) IN ((1,'jack','uk'),(2,'mike','usa'))
db.Where("(id, name, address) IN ?", [][]any{{1, "jack", "uk"}, {2, "mike", "usa"}}).Find(&ps)

Gorm hỗ trợ sử dụng nhóm where, kết hợp các statement trên

go
db.Where(
    db.Where("name IN ?", []string{"cn", "uk"}).Where("id IN ?", []uint{1, 2}),
  ).Or(
    db.Where("name IN ?", []string{"usa", "jp"}).Where("id IN ?", []uint{3, 4}),
  ).Find(&ps)
// SELECT * FROM `person` WHERE (name IN ('cn','uk') AND id IN (1,2)) OR (name IN ('usa','jp') AND id IN (3,4))

Order

Sort sẽ sử dụng method Order

go
func (db *DB) Order(value interface{}) (tx *DB)

Xem ví dụ sử dụng

go
var ps []Person

// SELECT * FROM `person` ORDER BY name ASC, id DESC
db.Order("name ASC, id DESC").Find(&ps)

Cũng có thể gọi nhiều lần

go
// SELECT * FROM `person` ORDER BY name ASC, id DESC,address
db.Order("name ASC, id DESC").Order("address").Find(&ps)

Limit

Method LimitOffset thường dùng cho query phân trang

go
func (db *DB) Limit(limit int) (tx *DB)

func (db *DB) Offset(offset int) (tx *DB)

Dưới đây là ví dụ phân trang đơn giản

go
var (
    ps   []Person
    page = 2
    size = 10
)

// SELECT * FROM `person` LIMIT 10 OFFSET 10
db.Offset((page - 1) * size).Limit(size).Find(&ps)

Group

Method GroupHaving thường dùng cho operation group

go
func (db *DB) Group(name string) (tx *DB)

func (db *DB) Having(query interface{}, args ...interface{}) (tx *DB)

Xem ví dụ

go
var (
    ps []Person
)

// SELECT `address` FROM `person` GROUP BY `address` HAVING address IN ('cn','us')
db.Select("address").Group("address").Having("address IN ?", []string{"cn", "us"}).Find(&ps)

Distinct

Method Distinct thường dùng cho deduplication

go
func (db *DB) Distinct(args ...interface{}) (tx *DB)

Xem ví dụ

go
// SELECT DISTINCT `name` FROM `person` WHERE address IN ('cn','us')
db.Where("address IN ?", []string{"cn", "us"}).Distinct("name").Find(&ps)

Subquery

Subquery là nested query, ví dụ muốn query tất cả người có id lớn hơn giá trị trung bình

go
// SELECT * FROM `person` WHERE id > (SELECT AVG(id) FROM `person`
db.Where("id > (?)", db.Model(Person{}).Select("AVG(id)")).Find(&ps)

From subquery

go
// SELECT * FROM (SELECT * FROM `person` WHERE address IN ('cn','uk')) as p
db.Table("(?) as p", db.Model(Person{}).Where("address IN ?", []string{"cn", "uk"})).Find(&ps)

Lock

Gorm sử dụng clause clause.Locking để cung cấp hỗ trợ lock

go
// SELECT * FROM `person` FOR UPDATE
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&ps)

// SELECT * FROM `person` FOR SHARE NOWAIT
db.Clauses(clause.Locking{Strength: "SHARE", Options: "NOWAIT"}).Find(&ps)

Iteration

Có thể lấy iterator thông qua method Rows

go
func (db *DB) Rows() (*sql.Rows, error)

Bằng cách duyệt iterator, sử dụng method ScanRows có thể scan kết quả của mỗi row vào struct.

go
rows, err := db.Model(Person{}).Rows()
if err != nil {
    return
}
defer rows.Close()

for rows.Next() {
    var p Person
    err := db.ScanRows(rows, &p)
    if err != nil {
        return
    }
}

Update

Save

Đã đề cập method Save khi create, nó cũng có thể dùng để update record, và nó sẽ update tất cả các trường, ngay cả khi một số trường struct là zero value, nhưng nếu primary key không khớp thì sẽ thực hiện insert.

go
var p Person

db.First(&p)

p.Address = "poland"
// UPDATE `person` SET `name`='json',`address`='poland' WHERE `id` = 2
db.Save(&p)

Có thể thấy nó đã thêm tất cả các trường trừ primary key vào statement SET.

Update

Vì vậy trong hầu hết trường hợp, khuyến nghị sử dụng method Update

go
func (db *DB) Update(column string, value interface{}) (tx *DB)

Nó chủ yếu dùng để update single column field

go
var p Person

db.First(&p)

// UPDATE `person` SET `address`='poland' WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Update("address", "poland")

Updates

Method Updates dùng để update multi-column, nhận struct và map làm tham số, và khi struct field là zero value, sẽ bỏ qua trường đó, nhưng trong map thì không.

go
func (db *DB) Updates(values interface{}) (tx *DB)

Dưới đây là ví dụ

go
var p Person

db.First(&p)

// UPDATE `person` SET `name`='jojo',`address`='poland' WHERE `id` = 2
db.Model(p).Updates(Person{Name: "jojo", Address: "poland"})

// UPDATE `person` SET `address`='poland',`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "address": "poland"})

SQL Expression

Đôi khi, thường cần thực hiện một số operation như increment hoặc decrement với chính field, thông thường là query trước rồi tính toán sau đó update, hoặc sử dụng SQL expression.

go
func Expr(expr string, args ...interface{}) clause.Expr

Xem ví dụ sau

go
// UPDATE `person` SET `age`=age + age,`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "age": gorm.Expr("age + age")})

// UPDATE `person` SET `age`=age * 2 + age,`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "age": gorm.Expr("age * 2 + age")})

Delete

Trong gorm, delete record sẽ sử dụng method Delete, có thể truyền entity struct, cũng có thể truyền condition.

go
func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB)

Ví dụ truyền struct

go
var p Person

db.First(&p)

// DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&p)

Hoặc

go
var p Person

db.First(&p)

// DELETE FROM `person` WHERE `person`.`id` = 2
db.Model(p).Delete(nil)

Hoặc chỉ định condition

go
// DELETE FROM `person` WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Delete(nil)

Cũng có thể viết gọn

go
var p Person

db.First(&p)

// DELETE FROM `person` WHERE id = 2
db.Delete(&Person{}, "id = ?", 2)

// DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&Person{}, 2)

Delete batch thì truyền slice

go
// DELETE FROM `person` WHERE id IN (1,2,3)
db.Delete(&Person{}, "id IN ?", []uint{1, 2, 3})
// DELETE FROM `person` WHERE `person`.`id` IN (1,2,3)
db.Delete(&Person{}, []uint{1, 2, 3})

Soft Delete

Nếu entity model của bạn sử dụng soft delete, khi delete, mặc định thực hiện update operation, nếu muốn delete vĩnh viễn có thể sử dụng method Unscoped

go
db.Unscoped().Delete(&Person{}, []uint{1, 2, 3})

Association Definition

Gorm cung cấp khả năng tương tác association table, định nghĩa association giữa các struct thông qua embed struct và field.

One-to-One

Relationship one-to-one là đơn giản nhất, bình thường một người chỉ có thể có một mẹ, xem struct sau

go
type Person struct {
  Id      uint
  Name    string
  Address string
  Age     uint

  MomId sql.NullInt64
  Mom   Mom `gorm:"foreignKey:MomId;"`
}

type Mom struct {
  Id   uint
  Name string
}

Struct Person thông qua embed struct Mom, thực hiện reference kiểu Mom, trong đó Person.MomId là field reference, primary key Mom.Id là field được reference, như vậy hoàn thành association one-to-one. Cách tùy chỉnh foreign key và reference và constraint và quy tắc foreign key mặc định đã được đề cập trong Foreign Key Definition, không nhắc lại

TIP

Đối với foreign key field, khuyến nghị sử dụng kiểu do package sql cung cấp, vì foreign key mặc định có thể là NULL, khi sử dụng Create để tạo record, nếu sử dụng kiểu thông thường, zero value 0 cũng sẽ được tạo, foreign key không tồn tại được tạo rõ ràng không được phép.

One-to-Many

Thêm một struct school, relationship giữa school và student là one-to-many, một school có nhiều student, nhưng một student chỉ có thể học ở một school.

go
type Person struct {
    Id      uint
    Name    string
    Address string
    Age     uint

    MomId sql.NullInt64
    Mom   Mom `gorm:"foreignKey:MomId;"`

    SchoolId sql.NullInt64
    School   School `gorm:"foreignKey:SchoolId;"`
}

type Mom struct {
    Id   uint
    Name string
}


type School struct {
    Id   uint
    Name string

    Persons []Person `gorm:"foreignKey:SchoolId;"`
}

school.Persons là kiểu []Person, biểu thị có thể sở hữu nhiều student, và Person thì phải包含 foreign key reference School, tức là Person.SchoolId.

Many-to-Many

Một người có thể sở hữu nhiều house, một house cũng có thể chứa nhiều người, đây là relationship many-to-many.

go
type Person struct {
  Id      uint
  Name    string
  Address string
  Age     uint

  MomId sql.NullInt64
  Mom   Mom `gorm:"foreignKey:MomId;"`

  SchoolId sql.NullInt64
  School   School `gorm:"foreignKey:SchoolId;"`

  Houses []House `gorm:"many2many:person_house;"`
}

type Mom struct {
  Id   uint
  Name string
}

type School struct {
  Id   uint
  Name string

  Persons []Person
}

type House struct {
  Id   uint
  Name string

  Persons []Person `gorm:"many2many:person_house;"`
}

type PersonHouse struct {
  PersonId sql.NullInt64
  Person   Person `gorm:"foreignKey:PersonId;"`
  HouseId  sql.NullInt64
  House    House `gorm:"foreignKey:HouseId;"`
}

PersonHouse互相 giữ slice của nhau biểu thị relationship many-to-many, relationship many-to-many thường cần tạo join table, chỉ định join table thông qua many2many, foreign key của join table phải được chỉ định chính xác.

Sau khi tạo struct xong để gorm tự động migrate vào database

go
tables := []any{
    School{},
    Mom{},
    Person{},
    House{},
    PersonHouse{},
}
for _, table := range tables {
    db.Migrator().CreateTable(&table)
}

Chú ý thứ tự tạo giữa bảng reference và bảng được reference.

Association Operation

Sau khi tạo xong ba relationship association trên, tiếp theo là cách sử dụng association để增删改查. Chủ yếu sẽ sử dụng method Association

go
func (db *DB) Association(column string) *Association

Nó nhận một tham số association, giá trị của nó nên là tên field của kiểu được reference trong struct embed reference.

go
db.Model(&person).Association("Mom").Find(&mom)

Ví dụ association query mẹ của một người, tham số AssociationMom, tức là tên field Person.Mom.

Create Association

go
// Định nghĩa dữ liệu
jenny := Mom{
    Name: "jenny",
}

mit := School{
    Name:    "MIT",
    Persons: nil,
}

h1 := House{
    Id:      0,
    Name:    "h1",
    Persons: nil,
}

h2 := House{
    Name:    "h2",
    Persons: nil,
}

jack := Person{
    Name:    "jack",
    Address: "usa",
    Age:     18,
}

mike := Person{
    Name:    "mike",
    Address: "uk",
    Age:     20,
}

// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`) VALUES ('jack','usa',18,NULL,NULL)
db.Create(&jack)
// INSERT INTO `schools` (`name`) VALUES ('MIT')
db.Create(&mit)

// Thêm association Person với Mom, association one-to-one
// INSERT INTO `moms` (`name`) VALUES ('jenny') ON DUPLICATE KEY UPDATE `id`=`id`
// UPDATE `people` SET `mom_id`=1 WHERE `id` = 1
db.Model(&jack).Association("Mom").Append(&jenny)

// Thêm association school với Person, association one-to-many
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`,`id`) VALUES ('jack','usa',18,1,1,1),('mike','uk',20,NULL,1,DEFAULT) ON DUPLICATE KEY UPDATE `school_id`=VALUES(`school_id`)
db.Model(&mit).Association("Persons").Append([]Person{jack, mike})

// Thêm association Person với Houses, association many-to-many
// INSERT INTO `houses` (`name`) VALUES ('h1'),('h2') ON DUPLICATE KEY UPDATE `id`=`id`
// INSERT INTO `person_house` (`person_id`,`house_id`) VALUES (1,1),(1,2) ON DUPLICATE KEY UPDATE `person_id`=`person_id`
db.Model(&jack).Association("Houses").Append([]House{h1, h2})

Nếu tất cả các record không tồn tại, khi thực hiện create association, cũng sẽ tạo record trước rồi tạo association.

Find Association

Dưới đây demo cách find association.

go
// Find association one-to-one
var person Person
var mom Mom

// SELECT * FROM `people` ORDER BY `people`.`id` LIMIT 1
db.First(&person)
// SELECT * FROM `moms` WHERE `moms`.`id` = 1
db.Model(person).Association("Mom").Find(&mom)

// Find association one-to-many
var school School
var persons []Person

// SELECT * FROM `schools` ORDER BY `schools`.`id` LIMIT 1
db.First(&school)
// SELECT * FROM `people` WHERE `people`.`school_id` = 1
db.Model(&school).Association("Persons").Find(&persons)

// Find association many-to-many
var houses []House

// SELECT `houses`.`id`,`houses`.`name` FROM `houses` JOIN `person_house` ON `person_house`.`house_id` = `houses`.`id` AND `person_house`.`person_id` IN (1,2)
db.Model(&persons).Association("Houses").Find(&houses)

Find association sẽ tìm record phù hợp trong reference table dựa trên dữ liệu hiện có, đối với relationship many-to-many, gorm sẽ tự động hoàn thành quá trình join table.

Update Association

Dưới đây demo cách update association

go
// Update association one-to-one
var jack Person

lili := Mom{
    Name: "lili",
}

// SELECT * FROM `people` WHERE name = 'jack' ORDER BY `people`.`id` LIMIT 1
db.Where("name = ?", "jack").First(&jack)

// INSERT INTO `moms` (`name`) VALUES ('lili')
db.Create(&lili)

// INSERT INTO `moms` (`name`,`id`) VALUES ('lili',2) ON DUPLICATE KEY UPDATE `id`=`id`
// UPDATE `people` SET `mom_id`=2 WHERE `id` = 1
db.Model(&jack).Association("Mom").Replace(&lili)

// Update association one-to-many

var mit School
newPerson := []Person{{Name: "bob"}, {Name: "jojo"}}
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`) VALUES ('bob','',0,NULL,NULL),('jojo','',0,NULL,NULL)
db.Create(&newPerson)

//  SELECT * FROM `schools` WHERE name = 'mit' ORDER BY `schools`.`id` LIMIT 1
db.Where("name = ?", "mit").First(&mit)

// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`,`id`) VALUES ('bob','',0,NULL,1,4),('jojo','',0,NULL,1,5) ON DUPLICATE KEY UPDATE `school_id`=VALUES(`school_id`)
//  UPDATE `people` SET `school_id`=NULL WHERE `people`.`id` NOT IN (4,5) AND `people`.`school_id` = 1
db.Model(&mit).Association("Persons").Replace(newPerson)

// Update association many-to-many

// INSERT INTO `houses` (`name`) VALUES ('h3'),('h4'),('h5') ON DUPLICATE KEY UPDATE `id`=`id`
// INSERT INTO `person_house` (`person_id`,`house_id`) VALUES (1,3),(1,4),(1,5) ON DUPLICATE KEY UPDATE `person_id`=`person_id`
// DELETE FROM `person_house` WHERE `person_house`.`person_id` = 1 AND `person_house`.`house_id` NOT IN (3,4,5)
db.Model(&jack).Association("Houses").Replace([]House{{Name: "h3"}, {Name: "h4"}, {Name: "h5"}})

Khi update association, nếu dữ liệu được reference và reference đều không tồn tại, gorm sẽ cố gắng tạo chúng.

Delete Association

Dưới đây demo cách delete association

go
// Delete association one-to-one
var (
    jack Person
    lili Mom
)

// SELECT * FROM `people` WHERE name = 'jack' ORDER BY `people`.`id` LIMIT 1
db.Where("name = ?", "jack").First(&jack)

//  SELECT * FROM `moms` WHERE name = 'lili' ORDER BY `moms`.`id` LIMIT 1
db.Where("name = ?", "lili").First(&lili)

// UPDATE `people` SET `mom_id`=NULL WHERE `people`.`id` = 1 AND `people`.`mom_id` = 2
db.Model(&jack).Association("Mom").Delete(&lili)

// Delete association one-to-many

var (
    mit     School
    persons []Person
)

// SELECT * FROM `schools` WHERE name = 'mit' ORDER BY `schools`.`id` LIMIT 1
db.Where("name = ?", "mit").First(&mit)
// SELECT * FROM `people` WHERE name IN ('jack','mike')
db.Where("name IN ?", []string{"jack", "mike"}).Find(&persons)

// UPDATE `people` SET `school_id`=NULL WHERE `people`.`school_id` = 1 AND `people`.`id` IN (1,2)
db.Model(&mit).Association("Persons").Delete(&persons)

// Delete association many-to-many
var houses []House

// SELECT * FROM `houses` WHERE name IN ('h3','h4')
db.Where("name IN ?", []string{"h3", "h4"}).Find(&houses)

// DELETE FROM `person_house` WHERE `person_house`.`person_id` = 1 AND `person_house`.`house_id` IN (3,4)
db.Model(&jack).Association("Houses").Delete(&houses)

Delete association chỉ xóa relationship reference giữa chúng, không xóa entity record. Cũng có thể sử dụng method Clear để xóa trực tiếp association

go
db.Model(&jack).Association("Houses").Clear()

Nếu muốn xóa entity record tương ứng, có thể thêm operation Unscoped sau operation Association (không ảnh hưởng many2many)

go
db.Model(&jack).Association("Houses").Unscoped().Delete(&houses)

Đối với one-to-many và many-to-many, có thể sử dụng operation Select để xóa record

go
var (
    mit     School
)
db.Where("name = ?", "mit").First(&mit)

db.Select("Persons").Delete(&mit)

Preload

Preload dùng để query association data, đối với entity có relationship association, nó sẽ preload entity được association reference trước. Query association đã đề cập trước đó là query cho relationship association, preload là query trực tiếp cho entity record, bao gồm tất cả relationship association. Về mặt syntax, query association cần query []Person chỉ định trước, sau đó dựa vào []Person để query []Mom association, preload từ syntax trực tiếp query []Person, và cũng sẽ load tất cả relationship association, nhưng thực tế SQL thực thi của chúng đều gần giống nhau. Xem ví dụ

go
var users []Person

// SELECT * FROM `moms` WHERE `moms`.`id` = 1
// SELECT * FROM `people`
db.Preload("Mom").Find(&users)

Đây là ví dụ query association one-to-one, output của nó

go
[{Id:1 Name:jack Address:usa Age:18 MomId:{Int64:1 Valid:true} Mom:{Id:1 Name:jenny} SchoolId:{Int64:1 Valid:true} School:{Id:0 Name: Persons:[]} Houses:[]} {Id:2 Name:mike Address:uk Age:20 MomId:{Int64:0 Valid:false} Mom:{Id:0 Name:} SchoolId:{Int64:1 Valid:true} School:{Id:0 Name: Persons:[]} Houses:[]}]

Có thể thấy đã query ra Mom association, nhưng không preload relationship school, tất cả struct School đều là zero value. Cũng có thể sử dụng clause.Associations để biểu thị preload tất cả relationship, trừ nested relationship.

go
db.Preload(clause.Associations).Find(&users)

Xem ví dụ nested preload, tác dụng của nó là query tất cả student của tất cả school association và mẹ mà mỗi student association và house mà mỗi student sở hữu, và còn query tập hợp chủ nhân của mỗi house, school->student->house->student.

go
var schools []School

db.Preload("Persons").
    Preload("Persons.Mom").
    Preload("Persons.Houses").
    Preload("Persons.Houses.Persons").Find(&schools)

// Output code, logic có thể bỏ qua
for _, school := range schools {
    fmt.Println("school", school.Name)
    for _, person := range school.Persons {
        fmt.Println("person", person.Name)
        fmt.Println("mom", person.Mom.Name)
        for _, house := range person.Houses {
            var persons []string
            for _, p := range house.Persons {
                persons = append(persons, p.Name)
            }
            fmt.Println("house", house.Name, "owner", persons)
        }
        fmt.Println()
    }
}

Output là

school MIT
person jack
mom jenny
house h1 owner [jack]
house h2 owner [jack]

person mike
mom

Có thể thấy output mẹ của mỗi student của mỗi school và house của chúng, và tất cả chủ nhân của house.

Transaction

Gorm mặc định开启 transaction, bất kỳ insert và update operation nào thất bại đều sẽ rollback, có thể tắt trong Connection Configuration, hiệu suất大概 tăng khoảng 30%. Việc sử dụng transaction trong gorm có nhiều cách, dưới đây giới thiệu đơn giản.

Automatic

Closure transaction, thông qua method Transaction, truyền một closure function, nếu giá trị trả về của function không nil, thì sẽ tự động rollback.

go
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error)

Xem ví dụ, operation trong closure nên sử dụng tham số tx, thay vì db bên ngoài.

go
var ps []Person

db.Transaction(func(tx *gorm.DB) error {
    err := tx.Create(&ps).Error
    if err != nil {
        return err
    }

    err = tx.Create(&ps).Error
    if err != nil {
        return err
    }

    err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
    if err != nil {
        return err
    }

    return nil
})

Manual

Khuyến nghị sử dụng manual transaction, do chúng ta tự kiểm soát khi nào rollback, khi nào commit. Manual transaction sẽ sử dụng ba method sau

go
// Method Begin dùng để开启 transaction
func (db *DB) Begin(opts ...*sql.TxOptions) *DB

// Method Rollback dùng để rollback transaction
func (db *DB) Rollback() *DB

// Method Commit dùng để commit transaction
func (db *DB) Commit() *DB

Xem ví dụ, sau khi开启 transaction, nên sử dụng tx để操作 orm.

go
var ps []Person

tx := db.Begin()

err := tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

err = tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
    tx.Rollback()
    return
}

tx.Commit()

Có thể chỉ định savepoint

go
var ps []Person

tx := db.Begin()

err := tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

tx.SavePoint("createBatch")

err = tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
    tx.RollbackTo("createBatch")
    return
}

tx.Commit()

Tổng kết

Nếu bạn đã đọc hết tất cả nội dung trên và đã gõ code, thì bạn có thể sử dụng gorm để thực hiện增删改查 database, ngoài những operation này, gorm còn có nhiều chức năng khác, chi tiết hơn có thể tìm hiểu tại tài liệu chính thức.

Golang by www.golangdev.cn edit