Skip to content

Gorm Database ORM Library

เอกสารอย่างเป็นทางการ: GORM - The fantastic ORM library for Golang, aims to be developer friendly.

ที่เก็บข้อมูลโอเพนซอร์ส: go-gorm/gorm: The fantastic ORM library for Golang, aims to be developer friendly (github.com)

ในชุมชน Go สำหรับส่วนการโต้ตอบกับฐานข้อมูล มีสองฝ่าย ฝ่ายหนึ่งชอบไลบรารีที่เรียบง่ายเช่น sqlx ซึ่งฟังก์ชันไม่ทรงพลังมากแต่สามารถควบคุม SQL ได้ตลอดเวลา ประสิทธิภาพสามารถปรับให้เหมาะสมที่สุด อีกฝ่ายหนึ่งชอบ ORM ที่เกิดมาเพื่อประสิทธิภาพการพัฒนา สามารถประหยัดปัญหาที่ไม่จำเป็นมากมายในกระบวนการพัฒนา เมื่อพูดถึง ORM ในชุมชนภาษา Go ไม่สามารถหลีกเลี่ยง gorm ได้ เป็น ORM ที่เก่าแก่มาก ที่คล้ายกันคือ xorm, ent และอื่นๆ ที่ค่อนข้างใหม่ บทความนี้พูดถึงเนื้อหาเกี่ยวกับ gorm บทความนี้เป็นเพียงการอธิบายเนื้อหาพื้นฐานของมัน ถือว่าเป็นการโยนอิฐเพื่อล่อหยก หากต้องการเข้าใจรายละเอียดลึกซึ้งมากขึ้นสามารถอ่านเอกสารทางการ เอกสารภาษาจีนของมันค่อนข้างสมบูรณ์แล้ว และผู้เขียนก็เป็นหนึ่งในทีมแปลเอกสาร gorm ด้วย

คุณสมบัติ

  • ORM แบบเต็มฟังก์ชัน
  • ความสัมพันธ์ (Has One, Has Many, Belongs To, Many To Many, Polymorphism, Single Table Inheritance)
  • เมธอด Hook ใน Create, Save, Update, Delete, Find
  • รองรับ Preload, Joins
  • ธุรกรรม, ธุรกรรมซ้อน, Save Point, Rollback To Saved Point
  • Context, Prepared Statement Mode, DryRun Mode
  • Batch Insert, FindInBatches, Find/Create with Map, CRUD ด้วย 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 Extensible Plugin API: Database Resolver (Multiple Databases, Read/Write Splitting), Prometheus...
  • ทุกคุณสมบัติผ่านการทดสอบอย่างเข้มงวด
  • Developer Friendly

gorm แน่นอนว่ามีข้อบกพร่องบ้าง เช่น พารามิเตอร์ของเมธอดเกือบทั้งหมดเป็นประเภท interface{} หากไม่ดูเอกสาร恐怕ไม่รู้ว่าควรส่งพารามิเตอร์อะไร บางครั้งสามารถส่งโครงสร้าง体 บางครั้งสามารถส่งสตริง บางครั้งสามารถส่ง map บางครั้งสามารถส่งสไลซ์ ความหมายค่อนข้างคลุมเครือ และหลายสถานการณ์ยังต้องเขียน SQL ด้วยตนเอง

มี orm สองตัวที่สามารถลองใช้เป็นทางเลือก ตัวแรกคือ aorm เพิ่งเปิดแหล่ง不久 ไม่ต้องเขียนชื่อฟิลด์ตารางด้วยตนเอง ส่วนใหญ่เป็นการดำเนินการแบบ chain อาศัย reflection เนื่องจากจำนวน star ไม่มาก สามารถรอดูอีก ตัวที่สองคือ ent เป็น orm ที่ facebook เปิดแหล่ง มันรองรับการดำเนินการแบบ chain เช่นกัน และส่วนใหญ่ไม่ต้องเขียน SQL ด้วยตนเอง แนวคิดการออกแบบของมันคือ基于กราฟ (กราฟในโครงสร้างข้อมูล) การ实现基于การสร้างโค้ด而非 reflection (ค่อนข้างเห็นด้วยกับสิ่งนี้) แต่เอกสารเป็นภาษาอังกฤษทั้งหมด มีเกณฑ์การเรียนรู้บ้าง

การติดตั้ง

ติดตั้งไลบรารี gorm

sh
$ go get -u gorm.io/gorm

การเชื่อมต่อ

gorm รองรับฐานข้อมูลต่อไปนี้

  • 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 รองรับโปรโตคอล mysql
  • ClickHouse:"gorm.io/driver/clickhouse"

นอกจากนี้ยังมีไดรเวอร์ฐานข้อมูลอื่นๆ ที่พัฒนาโดยผู้พัฒนาบุคคลที่สาม เช่น ไดรเวอร์ oracle ของ CengSin/oracle บทความต่อไปนี้จะใช้ MySQL ในการสาธิต ใช้ฐานข้อมูลอะไร ก็ต้องติดตั้งไดรเวอร์นั้น ที่นี่ติดตั้งไดรเวอร์ gorm ของ Mysql

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

จากนั้นเชื่อมต่อกับฐานข้อมูลโดยใช้ dsn (data source name) ไลบรารีไดรเวอร์จะแยกวิเคราะห์ dsn เป็นการกำหนดค่าที่สอดคล้องกัน

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")
}

หรือส่งการกำหนดค่าด้วยตนเอง

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")
}

สองวิธีนี้เทียบเท่ากัน ขึ้นอยู่กับนิสัยการใช้ของตัวเอง

การกำหนดค่าการเชื่อมต่อ

ผ่านการส่งโครงสร้าง体การกำหนดค่า gorm.Config เราสามารถควบคุมพฤติกรรมบางอย่างของ gorm

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

ต่อไปนี้เป็นคำอธิบายอย่างง่าย สามารถใช้ตามความต้องการของตัวเอง

go
type Config struct {
  // ปิดใช้งานธุรกรรมเริ่มต้น gorm จะเปิดธุรกรรมเมื่อสร้างและอัปเดตเดียวเพื่อรักษาความสอดคล้องของข้อมูล
  SkipDefaultTransaction bool
  // กลยุทธ์การตั้งชื่อที่กำหนดเอง
  NamingStrategy schema.Namer
  // บันทึกความสัมพันธ์แบบเต็ม
  FullSaveAssociations bool
  // logger ที่กำหนดเอง
  Logger logger.Interface
  // nowfunc ที่กำหนดเอง ใช้สำหรับ注入ฟิลด์ CreatedAt และ UpdatedAt
  NowFunc func() time.Time
  // สร้าง sql เท่านั้นไม่ดำเนินการ
  DryRun bool
  // ใช้ prepared statement
  PrepareStmt bool
  // หลังจากสร้างการเชื่อมต่อ ping ฐานข้อมูล
  DisableAutomaticPing bool
  // ละเว้น foreign key เมื่อ migration ฐานข้อมูล
  DisableForeignKeyConstraintWhenMigrating bool
  // ละเว้น relationship reference เมื่อ migration ฐานข้อมูล
  IgnoreRelationshipsWhenMigrating bool
  // ปิดใช้งาน nested transaction
  DisableNestedTransaction bool
  // อนุญาต global update คือ update without where
  AllowGlobalUpdate bool
  // query ฟิลด์ทั้งหมดของตาราง
  QueryFields bool
  // size ของ batch create
  CreateBatchSize int
  // เปิดใช้งาน error conversion
  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
}

โมเดล

ใน gorm โมเดลสอดคล้องกับตารางฐานข้อมูล มักแสดงโดยโครงสร้าง体 เช่น โครงสร้าง体ต่อไปนี้

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

ภายในโครงสร้าง体สามารถประกอบด้วยประเภทข้อมูลพื้นฐานและประเภทที่ใช้งานอินเทอร์เฟซ sql.Scanner และ sql.Valuer โดยค่าเริ่มต้น ตารางที่โครงสร้าง体 Person แมปคือ persons เป็นสไตล์พหูพจน์ snake_case คั่นด้วยขีดล่างใต้ ชื่อคอลัมน์ก็เป็นสไตล์ snake_case เช่น Id สอดคล้องกับชื่อคอลัมน์ id gorm ยังมีวิธีการกำหนดค่าบางอย่าง

ระบุชื่อคอลัมน์

ผ่านแท็กโครงสร้าง体 เราสามารถระบุชื่อคอลัมน์สำหรับฟิลด์โครงสร้าง体 เช่นนี้ gorm จะใช้ชื่อคอลัมน์ที่ระบุเมื่อทำ entity mapping

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

ระบุชื่อตาราง

ผ่านการใช้งานอินเทอร์เฟซ Table สามารถระบุชื่อตาราง它有เพียงหนึ่งเมธอด คือ ส่งคืนชื่อตาราง

go
type Tabler interface {
  TableName() string
}

ในเมธอดที่ใช้งาน มันส่งคืนสตริง person เมื่อทำการ migrate ฐานข้อมูล gorm จะสร้างตารางชื่อ 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"
}

สำหรับกลยุทธ์การตั้งชื่อ สามารถส่งการใช้งานกลยุทธ์ของตัวเองเมื่อสร้างการเชื่อมต่อเพื่อให้ได้ผลลัพธ์ที่กำหนดเอง

การติดตามเวลา

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"
}

เมื่อมีฟิลด์ CreatedAt หรือ UpdatedAt เมื่อสร้างหรืออัปเดตเรคคอร์ด หากเป็นค่าศูนย์ gorm จะใช้ time.Now() เพื่อตั้งค่าเวลาโดยอัตโนมัติ

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 รองรับ การติดตาม 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;"`
}

เมื่อ执行 Create เทียบเท่ากับ SQL ต่อไปนี้

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

ในสถานการณ์จริง หากมีความต้องการติดตามเวลา แนะนำให้ backend เก็บ timestamp ในกรณีข้ามเขตเวลา การจัดการง่ายกว่า

Model

gorm ให้โครงสร้าง体 Model ที่กำหนดไว้ล่วงหน้า它有 ID primary key และฟิลด์ติดตามเวลาสองฟิลด์ และฟิลด์ soft delete หนึ่งฟิลด์

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

เมื่อใช้งานเพียง embed เข้าใน entity model ของคุณ

go
type Order struct {
  gorm.Model
  Name string
}

เช่นนี้มันจะมีคุณสมบัติทั้งหมดของ gorm.Model โดยอัตโนมัติ

Primary Key

โดยค่าเริ่มต้น ฟิลด์ชื่อ Id คือ primary key ใช้แท็กโครงสร้าง体สามารถระบุฟิลด์ primary key

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

  CreatedAt sql.NullTime
  UpdatedAt sql.NullTime
}

หลายฟิลด์สร้าง 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

ผ่านแท็กโครงสร้าง体 index สามารถระบุ column 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;"`
}

ในโครงสร้าง体ด้านบน สร้าง unique index สำหรับฟิลด์ Address สองฟิลด์ใช้ index ชื่อเดียวกันจะสร้าง 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

ในโครงสร้าง体 กำหนดความสัมพันธ์ foreign key ผ่านการ embed โครงสร้าง体 เช่น

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;"`
}

ในตัวอย่าง โครงสร้าง体 Person มี foreign key สองตัว อ้างอิง primary key ของโครงสร้าง体 Dad และ Mom โดยค่าเริ่มต้นคืออ้างอิง primary key Person สำหรับ Dad และ Mom เป็นความสัมพันธ์ one-to-one คนหนึ่งมีพ่อและแม่ได้เพียง一人 Dad และ Mom สำหรับ Person เป็นความสัมพันธ์ one-to-many เพราะพ่อและแม่สามารถมีลูกได้หลายคน

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

หน้าที่ของ embed โครงสร้าง体คือ为了方便ระบุ foreign key และ reference โดยค่าเริ่มต้น รูปแบบชื่อฟิลด์ foreign key คือ ชื่อประเภทที่อ้างอิง+Id เช่น MomId โดยค่าเริ่มต้นคืออ้างอิง primary key ผ่านแท็กโครงสร้าง体สามารถระบุให้ reference ฟิลด์ใดฟิลด์หนึ่ง

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;"`
}

其中 constraint:OnUpdate:CASCADE,OnDelete:SET NULL; คือการกำหนด foreign key constraint

Hook

entity model สามารถกำหนด hook เองได้

  • Create
  • Update
  • Delete
  • Query

อินเทอร์เฟซที่สอดคล้องกันมีดังนี้

go
// เรียกก่อนสร้าง
type BeforeCreateInterface interface {
    BeforeCreate(*gorm.DB) error
}

// เรียกหลังสร้าง
type AfterCreateInterface interface {
    AfterCreate(*gorm.DB) error
}

// เรียกก่อนอัปเดต
type BeforeUpdateInterface interface {
    BeforeUpdate(*gorm.DB) error
}

// เรียกหลังอัปเดต
type AfterUpdateInterface interface {
    AfterUpdate(*gorm.DB) error
}

// เรียกก่อนบันทึก
type BeforeSaveInterface interface {
    BeforeSave(*gorm.DB) error
}

// เรียกหลังบันทึก
type AfterSaveInterface interface {
    AfterSave(*gorm.DB) error
}

// เรียกก่อนลบ
type BeforeDeleteInterface interface {
    BeforeDelete(*gorm.DB) error
}

// เรียกหลังลบ
type AfterDeleteInterface interface {
    AfterDelete(*gorm.DB) error
}

// เรียกหลังค้นหา
type AfterFindInterface interface {
    AfterFind(*gorm.DB) error
}

โครงสร้าง体ผ่านการใช้อินเทอร์เฟซเหล่านี้ สามารถกำหนดพฤติกรรมบางอย่างได้

Tags

ต่อไปนี้เป็น tags บางอย่างที่ gorm รองรับ

ชื่อแท็กคำอธิบาย
columnระบุชื่อคอลัมน์ db
typeประเภทข้อมูลคอลัมน์ แนะนำให้ใช้ประเภททั่วไปที่เข้ากันได้ดี เช่น bool, int, uint, float, string, time, bytes ที่ฐานข้อมูลทั้งหมดรองรับ และสามารถใช้กับแท็กอื่นได้ เช่น not null, size, autoIncrement... การระบุประเภทข้อมูลฐานข้อมูลเช่น varbinary(8) ก็รองรับ เมื่อใช้ประเภทข้อมูลฐานข้อมูลที่ระบุ ต้องเป็นประเภทข้อมูลฐานข้อมูลที่สมบูรณ์ เช่น MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializerระบุ serializer สำหรับ serialize หรือ deserialize ข้อมูลไปยังฐานข้อมูล เช่น serializer:json/gob/unixtime
sizeกำหนดขนาดหรือความยาวของประเภทข้อมูลคอลัมน์ เช่น size: 256
primaryKeyกำหนดคอลัมน์เป็น primary key
uniqueกำหนดคอลัมน์เป็น unique key
defaultกำหนดค่าเริ่มต้นของคอลัมน์
precisionระบุความแม่นยำของคอลัมน์
scaleระบุขนาดของคอลัมน์
not nullระบุคอลัมน์เป็น NOT NULL
autoIncrementระบุคอลัมน์เป็น auto increment
autoIncrementIncrementauto increment increment ควบคุมช่วงระหว่างเรคคอร์ดต่อเนื่อง
embeddedembedded field
embeddedPrefixcolumn name prefix ของ embedded field
autoCreateTimeติดตามเวลาปัจจุบันเมื่อสร้าง สำหรับฟิลด์ int จะติดตาม timestamp วินาที สามารถใช้ nano/milli เพื่อติดตาม timestamp นาโนวินาที มิลลิวินาที เช่น autoCreateTime:nano
autoUpdateTimeติดตามเวลาปัจจุบันเมื่อสร้าง/อัปเดต สำหรับฟิลด์ int จะติดตาม timestamp วินาที สามารถใช้ nano/milli เพื่อติดตาม timestamp นาโนวินาที มิลลิวินาที เช่น autoUpdateTime:milli
indexสร้าง index ตามพารามิเตอร์ หลายฟิลด์ใช้ชื่อเดียวกันจะสร้าง composite index ดู Indexes สำหรับรายละเอียด
uniqueIndexเหมือนกับ index แต่สร้าง unique index
checkสร้าง check constraint เช่น check:age > 13 ดู Constraints สำหรับรายละเอียด
<-ตั้งค่าสิทธิ์การเขียนของฟิลด์ <-:create สร้างเท่านั้น <-:update อัปเดตเท่านั้น <-:false ไม่มีสิทธิ์การเขียน <- สร้างและอัปเดต
->ตั้งค่าสิทธิ์การอ่านของฟิลด์ ->:false ไม่มีสิทธิ์การอ่าน
-ละเว้นฟิลด์นี้ - หมายถึงไม่มีอ่านเขียน -:migration หมายถึงไม่มีสิทธิ์ migration -:all หมายถึงไม่มีอ่านเขียน migration
commentเพิ่ม comment สำหรับฟิลด์เมื่อ migration
foreignKeyระบุคอลัมน์ของโมเดลปัจจุบันเป็น foreign key ของ connection table
referencesระบุชื่อคอลัมน์ของตารางที่อ้างอิง ซึ่งจะถูกแมปเป็น foreign key ของ connection table
polymorphicระบุ polymorphic type เช่น ชื่อโมเดล
polymorphicValueระบุ polymorphic value ค่าเริ่มต้นคือชื่อตาราง
many2manyระบุชื่อตาราง connection table
joinForeignKeyระบุชื่อคอลัมน์ foreign key ของ connection table ซึ่งจะถูกแมปกับตารางปัจจุบัน
joinReferencesระบุชื่อคอลัมน์ foreign key ของ connection table ซึ่งจะถูกแมปกับตารางที่อ้างอิง
constraintrelationship constraint เช่น OnUpdate, OnDelete

Migration

เมธอด AutoMigrate จะช่วยเราทำการ migrate อัตโนมัติ จะสร้างตาราง constraint index foreign key ฯลฯ

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

เช่น

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`))

หรือเราสามารถดำเนินการด้วยตนเอง ผ่านเมธอด Migrator เข้าถึงอินเทอร์เฟซ Migrator

go
func (db *DB) Migrator() Migrator

รองรับเมธอดอินเทอร์เฟซต่อไปนี้

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)
}

ในรายการเมธอดเกี่ยวข้องกับ database, table, column, view, index, constraint หลายมิติ สำหรับผู้ใช้ที่ต้องการกำหนดเองสามารถดำเนินการได้ละเอียดยิ่งขึ้น

ระบุ Table Comment

เมื่อ migrate หากต้องการเพิ่ม table comment สามารถตั้งค่าตามวิธีการต่อไปนี้

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

ควรทราบว่าหากใช้เมธอด AutoMigrate() ในการ migrate และโครงสร้าง体มีความสัมพันธ์การอ้างอิง gorm จะทำการ recursive สร้างตารางที่อ้างอิงก่อน ซึ่งจะทำให้ table comment ของตารางที่อ้างอิงและตารางที่ถูอ้างอิงซ้ำกัน ดังนั้นแนะนำให้ใช้เมธอด CreateTable ในการสร้าง

TIP

เมื่อสร้างตาราง เมธอด CreateTable ต้องรับประกันว่าตารางที่ถูอ้างอิงสร้างก่อนตารางที่อ้างอิง มิฉะนั้นจะเกิดข้อผิดพลาด ส่วนเมธอด AutoMigrate ไม่ต้อง เพราะมันจะ顺着ความสัมพันธ์การอ้างอิง recursive สร้าง

Create

Create

เมื่อสร้างเรคคอร์ดใหม่ ส่วนใหญ่จะใช้เมธอด Create

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

มีโครงสร้าง体ดังนี้

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

สร้างหนึ่งเรคคอร์ด

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

// ต้องส่ง reference
db = db.Create(&user)

// ข้อผิดพลาดที่เกิดขึ้นระหว่างดำเนินการ
err = db.Error
// จำนวนที่สร้าง
affected := db.RowsAffected

หลังสร้างเสร็จ gorm จะเขียน primary key ลงในโครงสร้าง体 user นี่คือเหตุผลว่าทำไมต้องส่ง pointer หากส่งเป็นสไลซ์ จะสร้างแบบ batch

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

db = db.Create(&user)

เช่นเดียวกัน gorm จะเขียน primary key ลงในสไลซ์ เมื่อข้อมูลมีปริมาณมากเกินไป สามารถใช้เมธอด CreateInBatches สร้างเป็น batch เพราะ SQL ที่สร้าง INSERT INTO table VALUES (),() จะยาวมาก แต่ละฐานข้อมูลมีจำกัดความยาว SQL ดังนั้นเมื่อจำเป็นสามารถเลือกสร้างเป็น batch

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

นอกจากนี้ เมธอด Save ก็สามารถสร้างเรคคอร์ดได้ หน้าที่ของมันคือเมื่อ primary key ตรงกันจะอัปเดตเรคคอร์ด มิฉะนั้นจะ insert

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

db = db.Save(&user)

Upsert

เมธอด Save สามารถจับคู่ primary key เท่านั้น เราสามารถสร้าง Clause เพื่อทำ upsert ที่กำหนดเองมากขึ้น เช่น โค้ดบรรทัดนี้

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

หน้าที่ของมันคือเมื่อฟิลด์ name ซ้ำกัน จะอัปเดตค่าของฟิลด์ address หากไม่ซ้ำกันจะสร้างเรคคอร์ดใหม่ สามารถทำอะไรไม่ได้เมื่อซ้ำกัน

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

หรืออัปเดตทุกฟิลด์โดยตรง

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

ก่อนใช้ upsert จำไว้ว่าเพิ่ม index ให้กับฟิลด์ที่ซ้ำกัน

Query

First

gorm สำหรับ query มีเมธอดให้เลือกใช้มากมาย ตัวแรกคือเมธอด First

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

หน้าที่ของมันคือค้นหาเรคคอร์ดแรกตาม primary key เรียง ascending เช่น

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

ส่ง pointer dest เพื่อ方便ให้ gorm แมปข้อมูลที่ค้นหากับโครงสร้าง体

หรือใช้เมธอด Table และ Model สามารถระบุตารางที่ค้นหา ตัวแรกรับสตริงชื่อตาราง ตัวหลังรับ entity model

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

TIP

หาก pointer ที่ส่งมี entity model เช่น pointer โครงสร้าง体 หรือ pointer สไลซ์โครงสร้าง体 ไม่ต้องระบุด้วยตนเองว่าค้นหาตารางไหน กฎนี้ใช้กับการดำเนินการเพิ่ม ลบ แก้ไข ค้นหาทั้งหมด

Take

เมธอด Take คล้ายกับ First ต่างกันตรงที่ไม่เรียงตาม 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

เมธอด Pluck ใช้สำหรับ query คอลัมน์เดียวของตารางแบบ batch ผลลัพธ์ที่ query สามารถรวบรวมลงในสไลซ์ของประเภทที่ระบุ ไม่จำเป็นต้องเป็นสไลซ์ของ entity type

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

เช่น รวบรวมที่อยู่ของทุกคนลงในสไลซ์สตริง

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)

จริงๆ แล้วเทียบเท่ากับ

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

Count

เมธอด Count ใช้สำหรับ统计จำนวนเรคคอร์ด entity

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

ดูตัวอย่างการใช้งาน

go
var count int64

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

Find

เมธอดที่ใช้บ่อยที่สุดสำหรับ query แบบ batch คือ Find

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

มันจะค้นหาเรคคอร์ดทั้งหมดที่ตรงตามเงื่อนไขที่กำหนด

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

Select

gorm โดยค่าเริ่มต้นคือ query ทุกฟิลด์ เราสามารถระบุฟิลด์ผ่านเมธอด Select

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

เช่น

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

เทียบเท่ากับ

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

ในขณะเดียวกัน สามารถใช้เมธอด Omit เพื่อละเว้นฟิลด์

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

เช่น

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)

ฟิลด์ที่เลือกหรือละเว้นโดย Select และ Omit จะมีผลเมื่อสร้าง query อัปเดต

Where

การ query แบบมีเงื่อนไขจะใช้เมธอด Where

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

ต่อไปนี้เป็นตัวอย่างอย่างง่าย

go
var p Person

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

ในการดำเนินการแบบ chain ใช้ Where หลายครั้งจะสร้างหลายคำสั่ง AND เช่น

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)

หรือใช้เมธอด Or เพื่อสร้างคำสั่ง 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)

还有เมธอด Not คล้ายกัน

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)

สำหรับเงื่อนไข IN สามารถส่งสไลซ์ในเมธอด Where โดยตรง

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

หรือเงื่อนไข IN หลายคอลัมน์ ต้องใช้ประเภท [][]any เพื่อ承载พารามิเตอร์

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 รองรับการใช้ where group就是将上述几个语句结合起来

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

การเรียงลำดับจะใช้เมธอด Order

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

ดูตัวอย่างการใช้งาน

go
var ps []Person

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

สามารถเรียกหลายครั้งได้

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

Limit

เมธอด Limit และ Offset มักใช้สำหรับ query แบบ分页

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

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

ต่อไปนี้เป็นตัวอย่างการ分页อย่างง่าย

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

เมธอด Group และ Having มักใช้สำหรับการดำเนินการ group

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

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

ดูตัวอย่าง

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

เมธอด Distinct มักใช้สำหรับการลบซ้ำ

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

ดูตัวอย่าง

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

Subquery

Subquery คือ nested query เช่น ต้องการ query คนทั้งหมดที่มีค่า id มากกว่าค่าเฉลี่ย

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 ใช้ clause clause.Locking เพื่อรองรับ 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

ผ่านเมธอด Rows สามารถได้ iterator

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

ผ่านการ遍历 iterator ใช้เมธอด ScanRows สามารถ scan ผลลัพธ์แต่ละแถวลงในโครงสร้าง体

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

ได้กล่าวถึงเมธอด Save เมื่อสร้าง มันสามารถใช้สำหรับอัปเดตเรคคอร์ดได้ และมันจะอัปเดตทุกฟิลด์ แม้ว่าบางฟิลด์ของโครงสร้าง体จะเป็นค่าศูนย์ แต่ถ้า primary key ไม่ตรงกันจะทำการ insert

go
var p Person

db.First(&p)

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

จะเห็นว่ามันใส่ทุกฟิลด์ยกเว้น primary key ลงในคำสั่ง SET

Update

ดังนั้นในสถานการณ์ส่วนใหญ่ แนะนำให้ใช้เมธอด Update

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

มันใช้สำหรับอัปเดตฟิลด์คอลัมน์เดียวเป็นหลัก

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

เมธอด Updates ใช้สำหรับอัปเดตหลายคอลัมน์ รับโครงสร้าง体และ map เป็นพารามิเตอร์ และเมื่อฟิลด์ของโครงสร้าง体เป็นค่าศูนย์ จะละเว้นฟิลด์นั้น แต่ใน map จะไม่

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

ต่อไปนี้เป็นตัวอย่าง

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

บางครั้ง มัก需要对ฟิลด์ทำการดำเนินการเช่น increment หรือ decrement หรือการคำนวณกับตัวเอง โดยทั่วไปคือ query ก่อนแล้วคำนวณแล้วอัปเดต หรือใช้ SQL expression

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

ดูตัวอย่างต่อไปนี้

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

ใน gorm การลบเรคคอร์ดจะใช้เมธอด Delete สามารถส่ง entity structure โดยตรง หรือส่งเงื่อนไข

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

เช่น ส่งโครงสร้าง体โดยตรง

go
var p Person

db.First(&p)

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

หรือ

go
var p Person

db.First(&p)

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

หรือระบุเงื่อนไข

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

หรือเขียนอย่างย่อ

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)

การลบแบบ batch คือส่งสไลซ์

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

หาก entity model ของคุณใช้ soft delete เมื่อลบ จะทำการ update โดยค่าเริ่มต้น หากต้องการลบอย่างถาวรสามารถใช้เมธอด Unscoped

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

Relationship Definition

gorm ให้ความสามารถในการโต้ตอบ table relationship ผ่านการ embed โครงสร้าง体和ฟิลด์เพื่อกำหนด relationship ระหว่างโครงสร้าง体

One To One

ความสัมพันธ์ one-to-one ง่ายที่สุด โดยปกติคนหนึ่งมีแม่ได้เพียง一人 ดูโครงสร้าง体ต่อไปนี้

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
}

โครงสร้าง体 Person ผ่านการ embed โครงสร้าง体 Mom ทำการ reference ประเภท Mom其中 Person.MomId คือฟิลด์ reference Mom.Id primary key คือฟิลด์ที่ถู reference เช่นนี้เสร็จสิ้นความสัมพันธ์ one-to-one วิธีการกำหนด foreign key และ reference และ constraint เอง以及กฎ foreign key เริ่มต้นได้กล่าวถึงใน Foreign Key Definition แล้ว ไม่กล่าวซ้ำ

TIP

สำหรับฟิลด์ foreign key แนะนำให้ใช้ประเภทที่ package sql ให้ เพราะ foreign key โดยค่าเริ่มต้นสามารถเป็น NULL เมื่อใช้ Create สร้างเรคคอร์ด หากใช้ประเภททั่วไป ค่าศูนย์ 0 ก็จะถูกสร้าง ซึ่ง foreign key ที่ไม่มีอยู่ถูกสร้าง显然ไม่ได้รับอนุญาต

One To Many

ต่อไปเพิ่มโครงสร้าง体 school ความสัมพันธ์ระหว่าง school และ student เป็น one-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;"`
}

type Mom struct {
    Id   uint
    Name string
}


type School struct {
    Id   uint
    Name string

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

school.Persons เป็นประเภท []person แสดงว่าสามารถมีนักเรียนได้หลายคน ส่วน Person ต้องมี foreign key ที่รวม reference School คือ Person.SchoolId

Many To Many

คน一人สามารถมีบ้านได้หลายหลัง บ้านหนึ่งหลังก็สามารถอยู่ได้หลายคน นี่คือความสัมพันธ์ 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;"`
}

Person และ House ถือสไลซ์ของกันและกันแสดงถึงความสัมพันธ์ many-to-many ความสัมพันธ์ many-to-many โดยทั่วไปต้องสร้าง connection table ผ่าน many2many เพื่อระบุ connection table foreign key ของ connection table ต้องระบุให้ถูกต้อง

หลังสร้างโครงสร้าง体แล้วให้ gorm migrate อัตโนมัติไปยังฐานข้อมูล

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

注意ลำดับการสร้างก่อนหลังของตารางที่อ้างอิงและตารางที่ถูอ้างอิง

Relationship Operation

หลังสร้างสามความสัมพันธ์ข้างต้นแล้ว ต่อไปคือ如何使用ความสัมพันธ์进行增删改查 นี้จะใช้เมธอด Association เป็นหลัก

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

มันรับพารามิเตอร์ association ค่าของมันควรเป็นชื่อฟิลด์ของประเภทที่ถู reference ที่ embed ในโครงสร้าง体 reference

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

เช่น association query แม่ของ一人 พารามิเตอร์ Association คือ Mom คือชื่อฟิลด์ Person.Mom

Create Association

go
// กำหนดข้อมูล
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)

// เพิ่ม association Person กับ 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)

// เพิ่ม association school กับ 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})

// เพิ่ม association Person กับ 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})

หากเรคคอร์ดทั้งหมดไม่มีอยู่ เมื่อทำการ create association จะสร้างเรคคอร์ดก่อนแล้วสร้าง association

Find Association

ต่อไปสาธิต如何进行 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 จะ根据ข้อมูลที่มีอยู่ ไป query เรคคอร์ดที่ตรงตามเงื่อนไขในตาราง reference สำหรับความสัมพันธ์ many-to-many gorm จะทำ table join อัตโนมัติ

Update Association

ต่อไปสาธิต如何进行 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"}})

เมื่อ update association หากข้อมูลที่ reference และข้อมูลที่ถู reference ไม่มีอยู่ gorm จะลองสร้างพวกมัน

Delete Association

ต่อไปสาธิต如何进行 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 จะลบเฉพาะความสัมพันธ์ reference ระหว่างพวกมัน ไม่ลบเรคคอร์ด entity เรายังสามารถใช้เมธอด Clear เพื่อ清空 association โดยตรง

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

หากต้องการลบเรคคอร์ด entity ที่สอดคล้องกัน สามารถเพิ่มการดำเนินการ Unscoped หลังการดำเนินการ Association (ไม่ส่งผลต่อ many2many)

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

สำหรับ one-to-many และ many-to-many สามารถใช้การดำเนินการ Select เพื่อลบเรคคอร์ด

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

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

Preload

Preload ใช้สำหรับ query association data สำหรับ entity ที่มีความสัมพันธ์ association มันจะ preload entity ที่ถู association reference ก่อนหน้านี้กล่าวถึง association query คือ query association relationship preload คือ query entity record โดยตรง รวมถึง association relationship ทั้งหมด จากไวยากรณ์ association query ต้อง query []Person ที่ระบุก่อน แล้ว根据 []Person ไป query []Mom ที่ association preload จากไวยากรณ์ query []Person โดยตรง และจะโหลด association relationship ทั้งหมดด้วย แต่จริงๆ แล้ว SQL ที่ดำเนินการ差不多เหมือนกัน ดูตัวอย่าง

go
var users []Person

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

นี่เป็นตัวอย่าง query association one-to-one เอาต์พุตของมัน

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:[]}]

จะเห็นว่า query association Mom ด้วย แต่ไม่ได้ preload school relationship ดังนั้น School structure体เป็นค่าศูนย์ทั้งหมด ยังสามารถใช้ clause.Associations เพื่อ表示 preload association ทั้งหมด ยกเว้น nested relationship

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

ต่อไปดูตัวอย่าง nested preload หน้าที่ของมันคือ query school association ทั้งหมดและ student ทั้งหมดที่แต่ละ school association และ mother ที่แต่ละ student association และ house ที่แต่ละ student มี และต้อง query set ของเจ้าของ house แต่ละหลัง school->student->house->student

go
var schools []School

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

// เอาต์พุตโค้ด ตรรกะสามารถละเว้น
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()
    }
}

เอาต์พุตคือ

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

person mike
mom

จะเห็นว่าเอาต์พุต mother ของ student แต่ละคนของ school แต่ละหลัง และ house ของพวกมัน และเจ้าของทั้งหมดของ house

Transaction

gorm เปิด transaction โดยค่าเริ่มต้น การ insert และ update ใดๆ ล้มเหลวจะ rollback สามารถปิดใน Connection Configuration ประสิทธิภาพจะเพิ่มขึ้นประมาณ 30% การใช้งาน transaction ใน gorm มีหลายวิธี ต่อไปแนะนำอย่างง่าย

Automatic

Closure transaction ผ่านเมธอด Transaction ส่ง closure function หากค่าส่งกลับของ function ไม่เป็น nil จะ rollback อัตโนมัติ

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

ดูตัวอย่าง การดำเนินการใน closure ควรใช้พารามิเตอร์ tx ไม่ใช่ db ภายนอก

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

แนะนำใช้ manual transaction เราควบคุมเองว่าเมื่อไหร่ rollback เมื่อไหร่ commit manual transaction จะใช้สามเมธอดต่อไปนี้

go
// เมธอด Begin ใช้สำหรับเปิด transaction
func (db *DB) Begin(opts ...*sql.TxOptions) *DB

// เมธอด Rollback ใช้สำหรับ rollback transaction
func (db *DB) Rollback() *DB

// เมธอด Commit ใช้สำหรับ commit transaction
func (db *DB) Commit() *DB

ดูตัวอย่าง หลังเปิด transaction ควรใช้ 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()

สามารถระบุ 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()

สรุป

หากคุณอ่านเนื้อหาทั้งหมดข้างต้นเสร็จ และลงมือเขียนโค้ด你就可以ใช้ gorm进行增删改查ฐานข้อมูลได้ gorm นอกจากการดำเนินการเหล่านี้แล้ว ยังมีฟังก์ชันอื่นๆ อีกมากมาย รายละเอียดเพิ่มเติมสามารถไปที่เอกสารทางการ了解

Golang by www.golangdev.cn edit