go mod init school-app # GORM core go get gorm.io/gorm # Driver PostgreSQL (dùng pgx — hiệu năng cao hơn lib/pq) go get gorm.io/driver/postgres
JSONB, UUID, Array.// Định dạng: user:pass@tcp(host:port)/db dsn := "root:pass@tcp(127.0.0.1:3306)/school?charset=utf8mb4&parseTime=True&loc=Local" db, err := gorm.Open( mysql.Open(dsn), &gorm.Config{}, )
// Định dạng: key=value (dễ đọc hơn) dsn := "host=localhost user=postgres password=pass dbname=school port=5432 sslmode=disable TimeZone=Asia/Ho_Chi_Minh" db, err := gorm.Open( postgres.Open(dsn), &gorm.Config{}, )
key=value thay vì URL-style của MySQL. Port mặc định PG là 5432, MySQL là 3306.package database import ( "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" "time" ) func Connect() *gorm.DB { dsn := "host=localhost user=postgres password=secret dbname=school port=5432 sslmode=disable TimeZone=Asia/Ho_Chi_Minh" db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Info), // in SQL khi dev }) if err != nil { log.Fatal("Không kết nối được PostgreSQL:", err) } sqlDB, _ := db.DB() sqlDB.SetMaxIdleConns(10) sqlDB.SetMaxOpenConns(100) sqlDB.SetConnMaxLifetime(time.Hour) return db }
| Tính năng | MySQL | PostgreSQL |
|---|---|---|
| Port mặc định | 3306 | 5432 |
| Auto increment | AUTO_INCREMENT | SERIAL hoặc BIGSERIAL |
| Kiểu chuỗi lớn | TEXT, LONGTEXT | TEXT (không giới hạn) |
| JSON | JSON | JSON + JSONB (có index!) |
| UUID | phải tự xử lý | kiểu native UUID |
| Array | không có | native INTEGER[], TEXT[] |
| Case sensitive | mặc định không | mặc định có (ILIKE để không phân biệt) |
| Backtick | `table_name` | "table_name" (dùng nháy kép) |
| Boolean | TINYINT(1) | kiểu native BOOLEAN |
| Limit offset | LIMIT x OFFSET y | LIMIT x OFFSET y (giống) |
docker run -e POSTGRES_PASSWORD=secret -p 5432:5432 postgres).gorm-pg-demo, cài GORM + driver postgres.Connect() kết nối đến database school."PostgreSQL kết nối thành công!" khi không có lỗi.type Student struct { gorm.Model // tự có: ID, CreatedAt, UpdatedAt, DeletedAt Name string `gorm:"size:100;not null"` Age int `gorm:"check:age > 0 AND age < 100"` Email string `gorm:"uniqueIndex;size:200"` Score float64 `gorm:"default:0;check:score >= 0 AND score <= 10"` ClassID uint } // Convention: struct "Student" → bảng "students" (tự động snake_case + plural)
// MySQL không có UUID native type Product struct { ID string `gorm:"primaryKey;size:36"` // phải tự generate UUID string Tags string // lưu JSON dưới dạng text // Không có Array native }
import "github.com/google/uuid" type Product struct { ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey"` // PG tự sinh UUID — không cần code Go! Tags pq.StringArray `gorm:"type:text[]"` Ratings pq.Int64Array `gorm:"type:integer[]"` Meta datatypes.JSON `gorm:"type:jsonb"` }
go get github.com/lib/pq (pq.StringArray) và go get gorm.io/datatypes (datatypes.JSON).| Tag | Ý nghĩa | Ví dụ PG |
|---|---|---|
column | Tên cột tùy chỉnh | gorm:"column:student_name" |
type | Kiểu SQL cụ thể | gorm:"type:uuid", gorm:"type:jsonb" |
size | Độ dài VARCHAR | gorm:"size:255" |
primaryKey | Khoá chính | gorm:"primaryKey" |
not null | Bắt buộc | gorm:"not null" |
unique | Giá trị duy nhất | gorm:"unique" |
uniqueIndex | Unique + có index | gorm:"uniqueIndex" |
index | Index thường | gorm:"index" |
default | Giá trị mặc định | gorm:"default:gen_random_uuid()" |
check | Ràng buộc CHECK | gorm:"check:score >= 0" |
- | Bỏ qua field | gorm:"-" |
embedded | Nhúng struct | gorm:"embedded;embeddedPrefix:addr_" |
serializer:json | Tự marshal/unmarshal JSON | gorm:"serializer:json" |
comment | Bình luận cột | gorm:"comment:Điểm học sinh" |
// Đặt tên bảng tùy ý func (Student) TableName() string { return "hs_students" } // Composite primary key (bảng trung gian) type Enrollment struct { StudentID uint `gorm:"primaryKey"` SubjectID uint `gorm:"primaryKey"` Score float64 EnrolledAt time.Time } // Embedded struct — tái sử dụng fields type Address struct { Street string City string } type Teacher struct { gorm.Model Name string Address Address `gorm:"embedded;embeddedPrefix:addr_"` // → cột: addr_street, addr_city }
Student với các field: name (not null), age, email (uniqueIndex), score (default 0, check 0-10).Product dùng UUID làm PK (default gen_random_uuid()).Tags pq.StringArray kiểu text[] vào Product — đây là kiểu PG không có trong MySQL.Address và nhúng vào Teacher với prefix addr_.student := Student{ Name: "Nguyễn Văn An", Age: 16, Email: "an@school.edu.vn", Score: 8.5, } result := db.Create(&student) if result.Error != nil { log.Println("Lỗi:", result.Error) } // Sau Create, GORM tự điền ID vào struct! fmt.Println("ID vừa tạo:", student.ID) fmt.Println("Rows affected:", result.RowsAffected)
&student) — GORM cần ghi ID trả về vào struct.// MySQL chỉ trả về ID sau INSERT db.Create(&student) fmt.Println(student.ID) // được // CreatedAt phải query lại mới có
// PG có thể lấy ngay CreatedAt sau INSERT db.Clauses(clause.Returning{}). Create(&student) // student.ID, student.CreatedAt, student.Score... đều được điền! // Chỉ lấy 1 số fields db.Clauses(clause.Returning{ Columns: []clause.Column{{Name: "id"}, {Name: "created_at"}}, }).Create(&student)
// Tạo nhiều bản ghi students := []Student{ {Name: "Trần Thị Bích", Age: 15, Email: "bich@school.edu.vn"}, {Name: "Lê Văn Cường", Age: 17, Email: "cuong@school.edu.vn"}, {Name: "Phạm Thị Dung", Age: 16, Email: "dung@school.edu.vn"}, } db.Create(&students) // Batch 100 / lần — tốt cho dữ liệu lớn db.CreateInBatches(&students, 100) // FirstOrCreate — tìm theo email, không có thì tạo var s Student db.Attrs(Student{Name: "Hoàng Em", Age: 16}). FirstOrCreate(&s, Student{Email: "em@school.edu.vn"})
// MySQL dùng ON DUPLICATE KEY UPDATE db.Clauses(clause.OnConflict{ Columns: []clause.Column{ {Name: "email"}, }, DoUpdates: clause.AssignmentColumns( []string{"name", "score"}, ), }).Create(&student)
// PG: giống cú pháp, nhưng chuẩn SQL hơn db.Clauses(clause.OnConflict{ Columns: []clause.Column{ {Name: "email"}, }, DoUpdates: clause.AssignmentColumns( []string{"name", "score"}, ), }).Create(&student) // Hoặc bỏ qua nếu conflict db.Clauses(clause.OnConflict{ DoNothing: true, }).Create(&student)
clause.Returning{}.CreateInBatches, batch size 2.FirstOrCreate — chạy 2 lần với cùng email, quan sát lần 2 không tạo mới.var s Student var ss []Student db.First(&s, 1) // SELECT * FROM students WHERE id=1 ORDER BY id LIMIT 1 db.Last(&s) // ORDER BY id DESC LIMIT 1 db.Take(&s, 1) // không có ORDER BY — nhanh hơn db.Find(&ss) // SELECT * FROM students db.Find(&ss, []int{1,2,3}) // WHERE id IN (1,2,3) // Xử lý "không tìm thấy" result := db.First(&s, 999) if errors.Is(result.Error, gorm.ErrRecordNotFound) { fmt.Println("Không tìm thấy") }
// MySQL LIKE mặc định case-insensitive db.Where("name LIKE ?", "%van%"). Find(&students) // Tìm được: "Văn An", "văn bình"...
// PG: LIKE phân biệt HOA/thường! db.Where("name LIKE ?", "%Van%").Find(&ss) // Chỉ tìm "Van" — không tìm "van" // Dùng ILIKE để không phân biệt (PG only) db.Where("name ILIKE ?", "%van%").Find(&ss) // Tìm được cả "Van", "van", "VAN"
var students []Student db.Where("age > ?", 15).Find(&students) db.Where("score BETWEEN ? AND ?", 7.0, 10.0).Find(&students) db.Where("name ILIKE ?", "%văn%").Find(&students) // PG: ILIKE db.Where("age IN ?", []int{15, 16, 17}).Find(&students) db.Not("class_id = ?", 3).Find(&students) db.Where("age = ?", 15).Or("age = ?", 16).Find(&students) // Map condition (không bỏ qua zero value) db.Where(map[string]interface{}{ "score": 0, // sẽ được lọc, không bị bỏ qua "age": 16, }).Find(&students)
0, "") bị bỏ qua. Dùng Map nếu cần lọc giá trị zero.page, size := 2, 10 var students []Student var total int64 // Đếm tổng db.Model(&Student{}).Count(&total) // Lấy trang db.Order("score DESC").Order("name ASC"). Limit(size).Offset((page-1)*size). Find(&students) fmt.Printf("Trang %d/%d — %d học sinh\n", page, (total+int64(size)-1)/int64(size), int64(len(students)))
// Chỉ lấy cột cần thiết var students []Student db.Select("name, score").Find(&students) // DTO — struct riêng để nhận kết quả type StudentDTO struct { Name string Score float64 } var dtos []StudentDTO db.Model(&Student{}).Select("name, score").Scan(&dtos) // Pluck — lấy 1 cột thành slice var names []string db.Model(&Student{}).Pluck("name", &names)
type Report struct { ClassName string Total int64 AvgScore float64 } var reports []Report db.Model(&Student{}). Select("classes.name as class_name, COUNT(students.id) as total, AVG(students.score) as avg_score"). Joins("JOIN classes ON classes.id = students.class_id"). Group("classes.name"). Having("AVG(students.score) > ?", 7.0). Order("avg_score DESC"). Scan(&reports)
JSONB — lưu JSON nhưng có thể đánh index và query trực tiếp trong SQL. MySQL chỉ có JSON không có index được.// Model có field JSONB type Product struct { gorm.Model Name string Meta datatypes.JSON `gorm:"type:jsonb"` // Meta lưu: {"color": "red", "size": "XL", "inStock": true} } // Query theo key trong JSONB (PG syntax) var products []Product // Lấy sản phẩm màu đỏ db.Where("meta->>'color' = ?", "red").Find(&products) // Lấy sản phẩm đang còn hàng db.Where("(meta->>'inStock')::boolean = true").Find(&products) // Dùng datatypes helper (gorm.io/datatypes) db.Where(datatypes.JSONQuery("meta").Equals("red", "color")).Find(&products)
-> trả về JSON, ->> trả về text. Ví dụ: meta->'color' = "red" (JSON), meta->>'color' = red (string).ILIKE (thử lại với LIKE để thấy sự khác biệt).Meta JSONB, lưu một vài sản phẩm, rồi query theo meta->>'color'.var s Student db.First(&s, 1) s.Score = 9.5 // Cập nhật TẤT CẢ columns! db.Save(&s) // SQL: UPDATE students SET name=..., age=..., // score=9.5, email=..., ... WHERE id=1 // Nguy hiểm nếu có field nào = 0 / ""
var s Student db.First(&s, 1) // Chỉ update score — an toàn hơn db.Model(&s).Update("score", 9.5) // SQL: UPDATE students SET score=9.5, // updated_at=NOW() WHERE id=1 // Nhiều cột dùng map (kể cả zero value) db.Model(&s).Updates(map[string]interface{}{ "score": 9.5, "name": "Tên mới", })
// Cộng thêm điểm (dùng giá trị hiện tại trong DB) db.Model(&Student{}).Where("id = ?", 1). Update("score", gorm.Expr("score + ?", 0.5)) // Tăng điểm tất cả học sinh lớp 1 lên 10% db.Model(&Student{}).Where("class_id = ?", 1). Updates(map[string]interface{}{ "score": gorm.Expr("LEAST(score * 1.1, 10)"), // LEAST: không vượt 10 })
LEAST(a, b) và GREATEST(a, b) rất tiện. MySQL cũng có nhưng ít biết hơn.var updated Student // Cập nhật và lấy lại bản ghi sau khi update — 1 round trip! db.Model(&Student{}).Where("id = ?", 1). Clauses(clause.Returning{}). Updates(Student{Score: 9.5, Name: "Tên mới"}) // MySQL: phải UPDATE rồi SELECT riêng = 2 round trips
var s Student db.First(&s, 1) s.Name = "Tên mới"; s.Score = 10.0; s.Age = 20 // Chỉ update Name, Score — bỏ qua Age db.Model(&s).Select("Name", "Score").Updates(&s) // Update tất cả TRỪ Age db.Model(&s).Omit("Age").Updates(&s)
Model + Update để cập nhật score của học sinh id=1 thành 9.5.gorm.Expr, không vượt quá 10 (dùng LEAST).Select.clause.Returning{} để lấy lại bản ghi sau update trong 1 lệnh.deleted_at = NOW() — dữ liệu vẫn còn trong DB, có thể khôi phục
DELETE FROM ... — xoá vĩnh viễn, không khôi phục được
gorm.Model (có field DeletedAt) → tự động dùng Soft Delete.
// ── Soft Delete ── var s Student db.First(&s, 1) db.Delete(&s) // SQL: UPDATE students SET deleted_at=NOW() WHERE id=1 // Xoá theo điều kiện db.Where("score < ?", 3.0).Delete(&Student{}) // Xoá theo nhiều ID db.Delete(&Student{}, []int{1, 2, 3}) // ── Hard Delete vĩnh viễn ── db.Unscoped().Delete(&s) // SQL: DELETE FROM students WHERE id=1
var students []Student // Mặc định GORM tự thêm: WHERE deleted_at IS NULL db.Find(&students) // chỉ học sinh chưa xoá // Lấy cả đã xoá lẫn chưa xoá db.Unscoped().Find(&students) // Chỉ lấy bản ghi đã xoá mềm db.Unscoped().Where("deleted_at IS NOT NULL").Find(&students) // Khôi phục — set deleted_at về NULL var s Student db.Unscoped().First(&s, 1) db.Unscoped().Model(&s).Update("deleted_at", nil)
// Phải query trước để lưu thông tin var s Student db.First(&s, 1) // 1 query name := s.Name db.Delete(&s) // 1 query log.Println("Xoá:", name)
// PG: xoá và lấy lại thông tin — 1 query! var deleted Student db.Clauses(clause.Returning{}). Delete(&deleted, 1) log.Println("Đã xoá:", deleted.Name)
Unscoped để tìm lại bản ghi vừa xoá.clause.Returning{}.type Class struct { gorm.Model Name string Teacher string Students []Student // HasMany — GORM biết ClassID là khoá ngoại } type Student struct { gorm.Model Name string Score float64 ClassID uint // khoá ngoại Class Class // BelongsTo } // Tạo lớp kèm học sinh trong 1 lệnh db.Create(&Class{ Name: "10A", Teacher: "Thầy Minh", Students: []Student{ {Name: "An", Score: 8.5}, {Name: "Bình", Score: 9.0}, }, })
type Student struct { gorm.Model Name string Subjects []Subject `gorm:"many2many:student_subjects;"` } type Subject struct { gorm.Model Name string Students []Student `gorm:"many2many:student_subjects;"` } // PG tự tạo bảng: student_subjects(student_id BIGINT, subject_id BIGINT) // Dùng kiểu BIGINT (không phải INT như MySQL mặc định) // Thêm môn học cho học sinh var s Student db.First(&s, 1) db.Model(&s).Association("Subjects").Append(&Subject{Name: "Toán"})
var class Class var classes []Class // Load kèm học sinh db.Preload("Students").First(&class, 1) // Preload có điều kiện — chỉ load học sinh giỏi db.Preload("Students", "score >= ?", 8.0).Find(&classes) // Preload lồng nhau: Class → Students → Subjects db.Preload("Students.Subjects").Find(&classes) // Load tất cả associations db.Preload(clause.Associations).Find(&classes) // Association Mode — quản lý liên kết var s Student db.First(&s, 1) count := db.Model(&s).Association("Subjects").Count() db.Model(&s).Association("Subjects").Delete(&Subject{ID: 2}) db.Model(&s).Association("Subjects").Replace([]Subject{{Name: "Hoá"}})
var classes []Class db.Find(&classes) // 1 query for _, c := range classes { fmt.Println(c.Students) // ↑ mỗi lần này = 1 query! // 10 lớp = 10 queries thêm = N+1 }
var classes []Class // Preload = 2 queries tổng (không phải N+1) db.Preload("Students").Find(&classes) for _, c := range classes { fmt.Println(c.Students) // Không query thêm — đã load sẵn! }
Create.Preload("Students") để lấy lớp kèm danh sách học sinh. In ra từng tên.Association("Subjects").Append.Students.Subjects — lấy tất cả lớp kèm học sinh kèm môn học mỗi người.| Thao tác | Trước | Sau |
|---|---|---|
| Create | BeforeSave → BeforeCreate | AfterCreate → AfterSave |
| Update | BeforeSave → BeforeUpdate | AfterUpdate → AfterSave |
| Delete | BeforeDelete | AfterDelete |
| Query | — | AfterFind |
error trong hook sẽ hủy toàn bộ thao tác và rollback (nếu có transaction).type Student struct { gorm.Model Name string Email string Password string Score float64 } // 1. Validate + chuẩn hoá trước khi lưu func (s *Student) BeforeSave(tx *gorm.DB) error { if s.Score < 0 || s.Score > 10 { return errors.New("điểm phải từ 0 đến 10") } // Chuẩn hoá email — chữ thường, bỏ khoảng trắng s.Email = strings.ToLower(strings.TrimSpace(s.Email)) // Viết hoa chữ cái đầu tên s.Name = cases.Title(language.Vietnamese).String(s.Name) return nil } // 2. Hash mật khẩu trước khi tạo func (s *Student) BeforeCreate(tx *gorm.DB) error { if !strings.HasPrefix(s.Password, "$2a$") { hashed, err := bcrypt.GenerateFromPassword([]byte(s.Password), 12) if err != nil { return err } s.Password = string(hashed) } return nil } // 3. Ghi log sau khi tạo func (s *Student) AfterCreate(tx *gorm.DB) error { log.Printf("[NEW] Học sinh: id=%d name=%s\n", s.ID, s.Name) return nil } // 4. Bảo vệ học sinh giỏi không bị xoá func (s *Student) BeforeDelete(tx *gorm.DB) error { if s.Score >= 9.0 { return errors.New("không thể xoá học sinh xuất sắc") } return nil }
BeforeSave tự động viết hoa chữ cái đầu của Name.BeforeCreate kiểm tra email có ký tự @ và . không, nếu không thì lỗi.AfterCreate ghi log ra console: "Đã tạo: {name} ({email})".BeforeDelete — không cho xoá học sinh nếu score >= 9.0.func enrollStudent(db *gorm.DB, studentID, classID uint) error { return db.Transaction(func(tx *gorm.DB) error { // Cập nhật lớp cho học sinh if err := tx.Model(&Student{}).Where("id = ?", studentID). Update("class_id", classID).Error; err != nil { return err // tự động ROLLBACK } // Tăng số học sinh trong lớp if err := tx.Model(&Class{}).Where("id = ?", classID). Update("student_count", gorm.Expr("student_count + 1")).Error; err != nil { return err // tự động ROLLBACK } return nil // tự động COMMIT }) }
tx := db.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() tx.Create(&Student{Name: "Học sinh 1"}) // Đánh dấu điểm khôi phục tx.SavePoint("sp1") tx.Create(&Student{Name: "Học sinh 2"}) // Rollback về sp1 — Học sinh 2 bị hủy, Học sinh 1 còn tx.RollbackTo("sp1") tx.Commit() // chỉ có Học sinh 1
// MySQL: CREATE TABLE, ALTER TABLE // tự động COMMIT (implicit commit) // Không rollback được! tx := db.Begin() tx.Exec("CREATE TABLE test ...") tx.Rollback() // Bảng vẫn tồn tại! ← nguy hiểm
// PG: DDL cũng nằm trong transaction! tx := db.Begin() tx.Exec("CREATE TABLE test (id SERIAL)") tx.Rollback() // Bảng KHÔNG tồn tại — rollback hoàn hảo! // Cực kỳ hữu ích khi viết migration
transferStudent(from, to, studentID uint): chuyển học sinh giữa 2 lớp, cập nhật student_count của cả 2 lớp trong 1 transaction.// Học sinh xuất sắc func Excellent(db *gorm.DB) *gorm.DB { return db.Where("score >= ?", 9.0) } // Học sinh theo lớp func InClass(classID uint) func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Where("class_id = ?", classID) } } // Phân trang func Paginate(page, size int) func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Offset((page - 1) * size).Limit(size) } } // Tìm theo tên (PG: ILIKE không phân biệt hoa/thường) func NameContains(keyword string) func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Where("name ILIKE ?", "%"+keyword+"%") // ILIKE: đặc trưng PG, không cần phân biệt hoa/thường } } // Sử dụng — kết hợp linh hoạt var students []Student db.Scopes(Excellent, InClass(2), Paginate(1, 10)). Order("score DESC"). Find(&students)
AgeRange(min, max int) lọc học sinh theo khoảng tuổi.NameContains(keyword) dùng ILIKE thay vì LIKE.InClass + AgeRange + Paginate trong 1 query — in ra SQL bằng Debug().// Raw SELECT — scan vào slice struct var students []Student db.Raw("SELECT * FROM students WHERE age > ? AND score > ?", 15, 7.0).Scan(&students) // Scan vào DTO type Report struct { ClassID uint; Count int; AvgScore float64 } var reports []Report db.Raw(`SELECT class_id, COUNT(*) as count, AVG(score) as avg_score FROM students GROUP BY class_id`).Scan(&reports) // Exec — INSERT / UPDATE / DELETE thủ công db.Exec("UPDATE students SET score = ? WHERE class_id = ?", 9.0, 2)
// MySQL 8+ mới có, cú pháp giống nhưng // không hỗ trợ tất cả window functions db.Raw(` SELECT name, score, RANK() OVER ( PARTITION BY class_id ORDER BY score DESC ) as rank_in_class FROM students `).Scan(&results)
type StudentRank struct { Name string Score float64 RankInClass int Percentile float64 } var results []StudentRank db.Raw(` SELECT name, score, RANK() OVER ( PARTITION BY class_id ORDER BY score DESC ) as rank_in_class, PERCENT_RANK() OVER ( ORDER BY score ) as percentile FROM students WHERE deleted_at IS NULL `).Scan(&results)
// CTE: tìm học sinh có điểm cao nhất mỗi lớp type TopStudent struct { ClassID uint; Name string; Score float64 } var tops []TopStudent db.Raw(` WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY class_id ORDER BY score DESC) AS rn FROM students WHERE deleted_at IS NULL ) SELECT class_id, name, score FROM ranked WHERE rn = 1 `).Scan(&tops)
var students []Student // Dùng @tên thay vì ? — dễ đọc hơn db.Raw("SELECT * FROM students WHERE name ILIKE @name OR email = @email", map[string]interface{}{ "name": "%an%", "email": "an@school.edu.vn", }, ).Scan(&students)
ROW_NUMBER() Window Function).Exec để reset điểm âm về 0.PERCENT_RANK() để tính phân vị điểm số mỗi học sinh.// Tạo / cập nhật tất cả bảng theo struct err := db.AutoMigrate( &Student{}, &Class{}, &Subject{}, &Teacher{}, ) if err != nil { log.Fatal("Migration thất bại:", err) } // AutoMigrate chỉ THÊM cột / index mới, KHÔNG XOÁ cột cũ
m := db.Migrator() // ── Bảng ── m.HasTable(&Student{}) // bool m.CreateTable(&Student{}) m.DropTable(&Student{}) m.RenameTable("students", "pupils") m.GetTables() // []string // ── Cột ── m.HasColumn(&Student{}, "Score") m.AddColumn(&Student{}, "Phone") m.DropColumn(&Student{}, "Phone") m.AlterColumn(&Student{}, "Score") m.RenameColumn(&Student{}, "Score", "GradePoint") // ── Index ── m.HasIndex(&Student{}, "idx_email") m.CreateIndex(&Student{}, "idx_email") m.DropIndex(&Student{}, "idx_email") // ── Constraint ── m.HasConstraint(&Student{}, "fk_class") m.CreateConstraint(&Student{}, "fk_class") m.DropConstraint(&Student{}, "fk_class")
// MySQL: nếu migration lỗi giữa chừng // các bước trước đó KHÔNG rollback được // → database ở trạng thái nửa vời func Migrate(db *gorm.DB) { db.AutoMigrate(&Student{}) db.AutoMigrate(&Class{}) // Lỗi ở đây → Student đã tạo, Class chưa }
func Migrate(db *gorm.DB) error { return db.Transaction(func(tx *gorm.DB) error { if err := tx.AutoMigrate(&Student{}); err != nil { return err // ROLLBACK — không có gì thay đổi! } if err := tx.AutoMigrate(&Class{}); err != nil { return err } return nil // COMMIT toàn bộ }) }
AutoMigrate cho 4 models bên trong 1 transaction (chỉ được trên PG).students có tồn tại bằng HasTable.phone vào bảng students bằng AddColumn.GetTables().// MySQL: FULLTEXT chỉ hỗ trợ // tiếng Anh tốt, tiếng Việt khó db.Where("MATCH(name) AGAINST(?)", "nguyen", ).Find(&students)
// PG: to_tsvector + to_tsquery // Có thể cấu hình unaccent cho tiếng Việt var students []Student db.Raw(` SELECT * FROM students WHERE to_tsvector('simple', name) @@ to_tsquery('simple', ?) `, "nguyen").Scan(&students) // Tạo GIN index để tăng tốc (trong migration) db.Exec(` CREATE INDEX IF NOT EXISTS idx_students_fts ON students USING GIN(to_tsvector('simple', name)) `)
type Student struct { gorm.Model Name string Hobbies pq.StringArray `gorm:"type:text[]"` Scores pq.Float64Array `gorm:"type:float[]"` } // Tạo học sinh với mảng db.Create(&Student{ Name: "An", Hobbies: pq.StringArray{"đọc sách", "bóng đá", "lập trình"}, Scores: pq.Float64Array{8.5, 9.0, 7.5}, }) // Query: tìm học sinh có hobby "bóng đá" var students []Student db.Where("? = ANY(hobbies)", "bóng đá").Find(&students) // Query: tìm học sinh có TẤT CẢ hobby này db.Where("hobbies @> ?", pq.StringArray{"đọc sách", "lập trình"}).Find(&students) // Query: tìm học sinh có ÍT NHẤT 1 hobby trong danh sách db.Where("hobbies && ?", pq.StringArray{"bóng đá", "võ thuật"}).Find(&students)
| Operator | Ý nghĩa | Ví dụ |
|---|---|---|
= ANY(arr) | Phần tử có trong mảng | "bóng đá" = ANY(hobbies) |
arr @> arr2 | arr chứa tất cả phần tử arr2 | hobbies @> '{đọc sách}' |
arr && arr2 | Hai mảng có phần tử chung | hobbies && '{bóng đá}' |
array_length(arr, 1) | Độ dài mảng | array_length(hobbies, 1) > 2 |
// pgx COPY — import 100,000 rows trong vài giây // Nhanh hơn INSERT 10-50x import "github.com/jackc/pgx/v5/pgxpool" func BulkInsertStudents(pool *pgxpool.Pool, students []Student) error { rows := make([][]interface{}, len(students)) for i, s := range students { rows[i] = []interface{}{s.Name, s.Age, s.Email, s.Score} } _, err := pool.CopyFrom( context.Background(), pgx.Identifier{"students"}, []string{"name", "age", "email", "score"}, pgx.CopyFromRows(rows), ) return err }
// Xem SQL được tạo ra db.Debug().Where("score > ?", 8).Find(&students) // DryRun — chỉ tạo SQL, không chạy stmt := db.Session(&gorm.Session{DryRun: true}). Where("class_id = ?", 1).Find(&students).Statement fmt.Println("SQL:", stmt.SQL.String()) fmt.Println("Params:", stmt.Vars) // Prepared statement — cache để query nhanh hơn fastDB := db.Session(&gorm.Session{PrepareStmt: true})
Hobbies pq.StringArray vào Student, tạo vài bản ghi với hobbies khác nhau.= ANY(hobbies).DryRun để in SQL của câu query phức tạp nhất bạn viết hôm nay.Teacher, Class, Student, Subject với đầy đủ quan hệ. Student có thêm Hobbies pq.StringArray và Metadata datatypes.JSON.GetClassReport(classID): tên lớp, giáo viên, danh sách học sinh + điểm, điểm TB — dùng Preload + GORM query.TopStudentsPerClass(): học sinh điểm cao nhất mỗi lớp — dùng Window Function ROW_NUMBER().SearchStudents(keyword): tìm theo tên bằng ILIKE + Scope + phân trang.FindByHobby(hobby): tìm học sinh có hobby cụ thể — dùng Array operator = ANY(hobbies).BulkImportScores([]ScoreRecord): Upsert điểm hàng loạt bằng OnConflict.BeforeSave: validate email, Score 0-10. AfterCreate: log ra console.DELETE ... RETURNING.hobbies và metadata, đo hiệu năng query trước/sau.school-app/ ├── main.go ├── database/ │ └── db.go // Connect + pool config ├── models/ │ ├── teacher.go │ ├── class.go │ ├── student.go // hooks, pq.StringArray, jsonb │ └── subject.go ├── services/ │ ├── class.go // GetClassReport │ ├── student.go // TopStudents, Search, FindByHobby │ └── import.go // BulkImportScores (Upsert) ├── scopes/ │ └── student.go // Excellent, InClass, Paginate, NameContains (ILIKE) └── seed/ └── seed.go
| Tính năng | MySQL | PostgreSQL |
|---|---|---|
| Case-insensitive search | LIKE (mặc định) | ILIKE phải dùng ILIKE |
| RETURNING | Không có | clause.Returning{} ✓ |
| UUID native | Không | gen_random_uuid() ✓ |
| Array type | Không | pq.StringArray, text[] ✓ |
| JSONB + index | JSON (không index) | JSONB + GIN index ✓ |
| Window Functions | MySQL 8+ (hạn chế) | Đầy đủ, mạnh hơn ✓ |
| CTE writeable | Không | WITH ... INSERT/UPDATE ✓ |
| DDL Transaction | Không (implicit commit) | Rollback được ✓ |
| Full-text search | FULLTEXT (tiếng Anh) | tsvector + GIN index ✓ |
| Bulk COPY | LOAD DATA INFILE | pgx CopyFrom (nhanh hơn) ✓ |
db. Debug(). Model(&Student{}). Select("name, score"). Joins("JOIN classes ON ..."). Preload("Subjects"). Where("score > ?", 8). Where("name ILIKE ?", "%van%"). Scopes(Excellent). Order("score DESC"). Limit(10).Offset(0). Find(&students)
// ILIKE .Where("name ILIKE ?", "%van%") // RETURNING .Clauses(clause.Returning{}) // Array ANY .Where("? = ANY(hobbies)", "bóng đá") // JSONB query .Where("meta->>'color' = ?", "red") // Window (Raw SQL) .Raw("... RANK() OVER (...)") // DDL trong transaction tx.AutoMigrate(&T{})