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
$ 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
$ go get -u gorm.io/driver/mysqlจากนั้นเชื่อมต่อกับฐานข้อมูลโดยใช้ dsn (data source name) ไลบรารีไดรเวอร์จะแยกวิเคราะห์ dsn เป็นการกำหนดค่าที่สอดคล้องกัน
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")
}หรือส่งการกำหนดค่าด้วยตนเอง
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
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})ต่อไปนี้เป็นคำอธิบายอย่างง่าย สามารถใช้ตามความต้องการของตัวเอง
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 โมเดลสอดคล้องกับตารางฐานข้อมูล มักแสดงโดยโครงสร้าง体 เช่น โครงสร้าง体ต่อไปนี้
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
type Person struct {
Id uint `gorm:"column:ID;"`
Name string `gorm:"column:Name;"`
Address string
Mom string
Dad string
}ระบุชื่อตาราง
ผ่านการใช้งานอินเทอร์เฟซ Table สามารถระบุชื่อตาราง它有เพียงหนึ่งเมธอด คือ ส่งคืนชื่อตาราง
type Tabler interface {
TableName() string
}ในเมธอดที่ใช้งาน มันส่งคืนสตริง person เมื่อทำการ migrate ฐานข้อมูล gorm จะสร้างตารางชื่อ person
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"
}สำหรับกลยุทธ์การตั้งชื่อ สามารถส่งการใช้งานกลยุทธ์ของตัวเองเมื่อสร้างการเชื่อมต่อเพื่อให้ได้ผลลัพธ์ที่กำหนดเอง
การติดตามเวลา
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() เพื่อตั้งค่าเวลาโดยอัตโนมัติ
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 ด้วย
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 ต่อไปนี้
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 หนึ่งฟิลด์
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}เมื่อใช้งานเพียง embed เข้าใน entity model ของคุณ
type Order struct {
gorm.Model
Name string
}เช่นนี้มันจะมีคุณสมบัติทั้งหมดของ gorm.Model โดยอัตโนมัติ
Primary Key
โดยค่าเริ่มต้น ฟิลด์ชื่อ Id คือ primary key ใช้แท็กโครงสร้าง体สามารถระบุฟิลด์ primary key
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
Address string
Mom string
Dad string
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
}หลายฟิลด์สร้าง composite primary key
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
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
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 โครงสร้าง体 เช่น
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 เพราะพ่อและแม่สามารถมีลูกได้หลายคน
Mom Mom `gorm:"foreignKey:MomId;"`หน้าที่ของ embed โครงสร้าง体คือ为了方便ระบุ foreign key และ reference โดยค่าเริ่มต้น รูปแบบชื่อฟิลด์ foreign key คือ ชื่อประเภทที่อ้างอิง+Id เช่น MomId โดยค่าเริ่มต้นคืออ้างอิง primary key ผ่านแท็กโครงสร้าง体สามารถระบุให้ reference ฟิลด์ใดฟิลด์หนึ่ง
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
อินเทอร์เฟซที่สอดคล้องกันมีดังนี้
// เรียกก่อนสร้าง
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 |
autoIncrementIncrement | auto increment increment ควบคุมช่วงระหว่างเรคคอร์ดต่อเนื่อง |
embedded | embedded field |
embeddedPrefix | column 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 ซึ่งจะถูกแมปกับตารางที่อ้างอิง |
constraint | relationship constraint เช่น OnUpdate, OnDelete |
Migration
เมธอด AutoMigrate จะช่วยเราทำการ migrate อัตโนมัติ จะสร้างตาราง constraint index foreign key ฯลฯ
func (db *DB) AutoMigrate(dst ...interface{}) errorเช่น
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
func (db *DB) Migrator() Migratorรองรับเมธอดอินเทอร์เฟซต่อไปนี้
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 สามารถตั้งค่าตามวิธีการต่อไปนี้
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
func (db *DB) Create(value interface{}) (tx *DB)มีโครงสร้าง体ดังนี้
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
}สร้างหนึ่งเรคคอร์ด
user := Person{
Name: "jack",
}
// ต้องส่ง reference
db = db.Create(&user)
// ข้อผิดพลาดที่เกิดขึ้นระหว่างดำเนินการ
err = db.Error
// จำนวนที่สร้าง
affected := db.RowsAffectedหลังสร้างเสร็จ gorm จะเขียน primary key ลงในโครงสร้าง体 user นี่คือเหตุผลว่าทำไมต้องส่ง pointer หากส่งเป็นสไลซ์ จะสร้างแบบ batch
user := []Person{
{Name: "jack"},
{Name: "mike"},
{Name: "lili"},
}
db = db.Create(&user)เช่นเดียวกัน gorm จะเขียน primary key ลงในสไลซ์ เมื่อข้อมูลมีปริมาณมากเกินไป สามารถใช้เมธอด CreateInBatches สร้างเป็น batch เพราะ SQL ที่สร้าง INSERT INTO table VALUES (),() จะยาวมาก แต่ละฐานข้อมูลมีจำกัดความยาว SQL ดังนั้นเมื่อจำเป็นสามารถเลือกสร้างเป็น batch
db = db.CreateInBatches(&user, 50)นอกจากนี้ เมธอด Save ก็สามารถสร้างเรคคอร์ดได้ หน้าที่ของมันคือเมื่อ primary key ตรงกันจะอัปเดตเรคคอร์ด มิฉะนั้นจะ insert
func (db *DB) Save(value interface{}) (tx *DB)user := []Person{
{Name: "jack"},
{Name: "mike"},
{Name: "lili"},
}
db = db.Save(&user)Upsert
เมธอด Save สามารถจับคู่ primary key เท่านั้น เราสามารถสร้าง Clause เพื่อทำ upsert ที่กำหนดเองมากขึ้น เช่น โค้ดบรรทัดนี้
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: false,
DoUpdates: clause.AssignmentColumns([]string{"address"}),
UpdateAll: false,
}).Create(&p)หน้าที่ของมันคือเมื่อฟิลด์ name ซ้ำกัน จะอัปเดตค่าของฟิลด์ address หากไม่ซ้ำกันจะสร้างเรคคอร์ดใหม่ สามารถทำอะไรไม่ได้เมื่อซ้ำกัน
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: true,
}).Create(&p)หรืออัปเดตทุกฟิลด์โดยตรง
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
UpdateAll: true,
}).Create(&p)ก่อนใช้ upsert จำไว้ว่าเพิ่ม index ให้กับฟิลด์ที่ซ้ำกัน
Query
First
gorm สำหรับ query มีเมธอดให้เลือกใช้มากมาย ตัวแรกคือเมธอด First
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB)หน้าที่ของมันคือค้นหาเรคคอร์ดแรกตาม primary key เรียง ascending เช่น
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
func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB)var person Person
result := db.Take(&person)
err := result.Error
affected := result.RowsAffectedPluck
เมธอด Pluck ใช้สำหรับ query คอลัมน์เดียวของตารางแบบ batch ผลลัพธ์ที่ query สามารถรวบรวมลงในสไลซ์ของประเภทที่ระบุ ไม่จำเป็นต้องเป็นสไลซ์ของ entity type
func (db *DB) Pluck(column string, dest interface{}) (tx *DB)เช่น รวบรวมที่อยู่ของทุกคนลงในสไลซ์สตริง
var adds []string
// SELECT `address` FROM `person` WHERE name IN ('jack','lili')
db.Model(Person{}).Where("name IN ?", []string{"jack", "lili"}).Pluck("address", &adds)จริงๆ แล้วเทียบเท่ากับ
db.Select("address").Where("name IN ?", []string{"jack", "lili"}).Find(&adds)Count
เมธอด Count ใช้สำหรับ统计จำนวนเรคคอร์ด entity
func (db *DB) Count(count *int64) (tx *DB)ดูตัวอย่างการใช้งาน
var count int64
// SELECT count(*) FROM `person`
db.Model(Person{}).Count(&count)Find
เมธอดที่ใช้บ่อยที่สุดสำหรับ query แบบ batch คือ Find
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB)มันจะค้นหาเรคคอร์ดทั้งหมดที่ตรงตามเงื่อนไขที่กำหนด
// SELECT * FROM `person`
var ps []Person
db.Find(&ps)Select
gorm โดยค่าเริ่มต้นคือ query ทุกฟิลด์ เราสามารถระบุฟิลด์ผ่านเมธอด Select
func (db *DB) Select(query interface{}, args ...interface{}) (tx *DB)เช่น
// SELECT `address`,`name` FROM `person` ORDER BY `person`.`id` LIMIT 1
db.Select("address", "name").First(&p)เทียบเท่ากับ
db.Select([]string{"address", "name"}).First(&p)ในขณะเดียวกัน สามารถใช้เมธอด Omit เพื่อละเว้นฟิลด์
func (db *DB) Omit(columns ...string) (tx *DB)เช่น
// 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
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB)ต่อไปนี้เป็นตัวอย่างอย่างง่าย
var p Person
db.Where("id = ?", 1).First(&p)ในการดำเนินการแบบ chain ใช้ Where หลายครั้งจะสร้างหลายคำสั่ง AND เช่น
// 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
func (db *DB) Or(query interface{}, args ...interface{}) (tx *DB)// 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 คล้ายกัน
func (db *DB) Not(query interface{}, args ...interface{}) (tx *DB)// 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 โดยตรง
db.Where("address IN ?", []string{"cn", "us"}).Find(&ps)หรือเงื่อนไข IN หลายคอลัมน์ ต้องใช้ประเภท [][]any เพื่อ承载พารามิเตอร์
// 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就是将上述几个语句结合起来
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
func (db *DB) Order(value interface{}) (tx *DB)ดูตัวอย่างการใช้งาน
var ps []Person
// SELECT * FROM `person` ORDER BY name ASC, id DESC
db.Order("name ASC, id DESC").Find(&ps)สามารถเรียกหลายครั้งได้
// SELECT * FROM `person` ORDER BY name ASC, id DESC,address
db.Order("name ASC, id DESC").Order("address").Find(&ps)Limit
เมธอด Limit และ Offset มักใช้สำหรับ query แบบ分页
func (db *DB) Limit(limit int) (tx *DB)
func (db *DB) Offset(offset int) (tx *DB)ต่อไปนี้เป็นตัวอย่างการ分页อย่างง่าย
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
func (db *DB) Group(name string) (tx *DB)
func (db *DB) Having(query interface{}, args ...interface{}) (tx *DB)ดูตัวอย่าง
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 มักใช้สำหรับการลบซ้ำ
func (db *DB) Distinct(args ...interface{}) (tx *DB)ดูตัวอย่าง
// 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 มากกว่าค่าเฉลี่ย
// SELECT * FROM `person` WHERE id > (SELECT AVG(id) FROM `person`
db.Where("id > (?)", db.Model(Person{}).Select("AVG(id)")).Find(&ps)from subquery
// 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
// 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
func (db *DB) Rows() (*sql.Rows, error)ผ่านการ遍历 iterator ใช้เมธอด ScanRows สามารถ scan ผลลัพธ์แต่ละแถวลงในโครงสร้าง体
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
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
func (db *DB) Update(column string, value interface{}) (tx *DB)มันใช้สำหรับอัปเดตฟิลด์คอลัมน์เดียวเป็นหลัก
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 จะไม่
func (db *DB) Updates(values interface{}) (tx *DB)ต่อไปนี้เป็นตัวอย่าง
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
func Expr(expr string, args ...interface{}) clause.Exprดูตัวอย่างต่อไปนี้
// 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 โดยตรง หรือส่งเงื่อนไข
func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB)เช่น ส่งโครงสร้าง体โดยตรง
var p Person
db.First(&p)
// DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&p)หรือ
var p Person
db.First(&p)
// DELETE FROM `person` WHERE `person`.`id` = 2
db.Model(p).Delete(nil)หรือระบุเงื่อนไข
// DELETE FROM `person` WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Delete(nil)หรือเขียนอย่างย่อ
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 คือส่งสไลซ์
// 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
db.Unscoped().Delete(&Person{}, []uint{1, 2, 3})Relationship Definition
gorm ให้ความสามารถในการโต้ตอบ table relationship ผ่านการ embed โครงสร้าง体和ฟิลด์เพื่อกำหนด relationship ระหว่างโครงสร้าง体
One To One
ความสัมพันธ์ one-to-one ง่ายที่สุด โดยปกติคนหนึ่งมีแม่ได้เพียง一人 ดูโครงสร้าง体ต่อไปนี้
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 หนึ่งโรงเรียนมีนักเรียนหลายคน แต่นักเรียน一人สามารถเรียนในโรงเรียนได้เพียงแห่งเดียว
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
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 อัตโนมัติไปยังฐานข้อมูล
tables := []any{
School{},
Mom{},
Person{},
House{},
PersonHouse{},
}
for _, table := range tables {
db.Migrator().CreateTable(&table)
}注意ลำดับการสร้างก่อนหลังของตารางที่อ้างอิงและตารางที่ถูอ้างอิง
Relationship Operation
หลังสร้างสามความสัมพันธ์ข้างต้นแล้ว ต่อไปคือ如何使用ความสัมพันธ์进行增删改查 นี้จะใช้เมธอด Association เป็นหลัก
func (db *DB) Association(column string) *Associationมันรับพารามิเตอร์ association ค่าของมันควรเป็นชื่อฟิลด์ของประเภทที่ถู reference ที่ embed ในโครงสร้าง体 reference
db.Model(&person).Association("Mom").Find(&mom)เช่น association query แม่ของ一人 พารามิเตอร์ Association คือ Mom คือชื่อฟิลด์ Person.Mom
Create Association
// กำหนดข้อมูล
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
// 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
// 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
// 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 โดยตรง
db.Model(&jack).Association("Houses").Clear()หากต้องการลบเรคคอร์ด entity ที่สอดคล้องกัน สามารถเพิ่มการดำเนินการ Unscoped หลังการดำเนินการ Association (ไม่ส่งผลต่อ many2many)
db.Model(&jack).Association("Houses").Unscoped().Delete(&houses)สำหรับ one-to-many และ many-to-many สามารถใช้การดำเนินการ Select เพื่อลบเรคคอร์ด
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 ที่ดำเนินการ差不多เหมือนกัน ดูตัวอย่าง
var users []Person
// SELECT * FROM `moms` WHERE `moms`.`id` = 1
// SELECT * FROM `people`
db.Preload("Mom").Find(&users)นี่เป็นตัวอย่าง query association one-to-one เอาต์พุตของมัน
[{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
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
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 อัตโนมัติ
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error)ดูตัวอย่าง การดำเนินการใน closure ควรใช้พารามิเตอร์ tx ไม่ใช่ db ภายนอก
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 จะใช้สามเมธอดต่อไปนี้
// เมธอด 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
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
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 นอกจากการดำเนินการเหล่านี้แล้ว ยังมีฟังก์ชันอื่นๆ อีกมากมาย รายละเอียดเพิ่มเติมสามารถไปที่เอกสารทางการ了解
