這是 Go 語言從零到 Web 應用系列的第十四篇。前面我們用
database/sql手寫 SQL 操作資料庫,現在來看看 ORM 和資料庫工具如何簡化這些工作。
ORM vs 原生 SQL
| 面向 | 原生 SQL (database/sql) | ORM (GORM) | SQL-first (sqlx) |
|---|---|---|---|
| 學習曲線 | 需要懂 SQL | 可以少寫 SQL | 需要懂 SQL |
| 開發速度 | 較慢(模板程式碼多) | 快(自動產生 SQL) | 中等 |
| SQL 控制力 | 完全控制 | 有限(複雜查詢困難) | 完全控制 |
| 效能 | 最佳 | 有反射開銷 | 接近原生 |
| Migration | 需自行處理 | 內建 AutoMigrate | 需自行處理 |
GORM
安裝:
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite # 或 postgres、mysql
Model 定義
package main
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
Email string `gorm:"uniqueIndex;not null" json:"email"`
Books []Book `gorm:"foreignKey:UserID" json:"books"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 軟刪除
}
type Book struct {
ID uint `gorm:"primaryKey" json:"id"`
Title string `gorm:"size:200;not null" json:"title"`
Author string `gorm:"size:100" json:"author"`
UserID uint `json:"user_id"`
}
連線與 AutoMigrate
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func main() {
db, err := gorm.Open(sqlite.Open("app.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// AutoMigrate 自動建立或更新表結構
db.AutoMigrate(&User{}, &Book{})
}
CRUD 操作
// Create
user := User{Name: "Jack", Email: "[email protected]"}
result := db.Create(&user)
// result.RowsAffected = 1
// user.ID 自動填入
// Read
var u User
db.First(&u, 1) // 找 ID=1
db.Where("email = ?", "[email protected]").First(&u) // 條件查詢
// 查詢多筆
var users []User
db.Where("name LIKE ?", "%Jack%").Find(&users)
// Update
db.Model(&user).Update("name", "Jack Tse")
db.Model(&user).Updates(User{Name: "Jack Tse", Email: "[email protected]"})
// Delete(軟刪除,因為 User 有 DeletedAt 欄位)
db.Delete(&user, 1)
// 真正刪除
db.Unscoped().Delete(&user, 1)
關聯查詢
// 建立使用者和書籍
db.Create(&User{
Name: "Jack",
Email: "[email protected]",
Books: []Book{
{Title: "Go in Action", Author: "William Kennedy"},
{Title: "The Go Programming Language", Author: "Donovan & Kernighan"},
},
})
// Preload 關聯資料
var user User
db.Preload("Books").First(&user, 1)
// user.Books 會自動填入
// 條件 Preload
db.Preload("Books", "author LIKE ?", "%Kennedy%").First(&user, 1)
Hook
// 在建立之前自動處理
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Email == "" {
return errors.New("email is required")
}
return nil
}
// 在刪除之後清理關聯
func (u *User) AfterDelete(tx *gorm.DB) error {
tx.Where("user_id = ?", u.ID).Delete(&Book{})
return nil
}
Transaction
err := db.Transaction(func(tx *gorm.DB) error {
user := User{Name: "Jack", Email: "[email protected]"}
if err := tx.Create(&user).Error; err != nil {
return err // 回傳 error 會自動 rollback
}
book := Book{Title: "New Book", UserID: user.ID}
if err := tx.Create(&book).Error; err != nil {
return err
}
return nil // 回傳 nil 會自動 commit
})
sqlx
安裝:
go get -u github.com/jmoiron/sqlx
go get -u github.com/mattn/go-sqlite3 # 或 pgx
連線
import (
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sqlx.Connect("sqlite3", "app.db")
if err != nil {
panic(err)
}
defer db.Close()
// 手動建表
db.MustExec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
}
Model 定義
type User struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Email string `db:"email" json:"email"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
type Book struct {
ID int `db:"id" json:"id"`
Title string `db:"title" json:"title"`
Author string `db:"author" json:"author"`
UserID int `db:"user_id" json:"user_id"`
}
CRUD 操作
// Create
result, err := db.NamedExec(
`INSERT INTO users (name, email) VALUES (:name, :email)`,
&User{Name: "Jack", Email: "[email protected]"},
)
id, _ := result.LastInsertId()
// Read:單筆
var user User
err = db.Get(&user, "SELECT * FROM users WHERE id = ?", 1)
// Read:多筆
var users []User
err = db.Select(&users, "SELECT * FROM users WHERE name LIKE ?", "%Jack%")
// Update
_, err = db.NamedExec(
`UPDATE users SET name = :name WHERE id = :id`,
&User{ID: 1, Name: "Jack Tse"},
)
// Delete
_, err = db.Exec("DELETE FROM users WHERE id = ?", 1)
NamedQuery
// 具名參數查詢
rows, err := db.NamedQuery(
`SELECT * FROM users WHERE name = :name AND email = :email`,
map[string]interface{}{
"name": "Jack",
"email": "[email protected]",
},
)
for rows.Next() {
var user User
rows.StructScan(&user)
}
In 語法
// IN 查詢
ids := []int{1, 2, 3}
query, args, err := sqlx.In("SELECT * FROM users WHERE id IN (?)", ids)
query = db.Rebind(query) // 轉換佔位符格式
var users []User
err = db.Select(&users, query, args...)
Transaction
tx, err := db.Beginx()
if err != nil {
return err
}
_, err = tx.NamedExec(
`INSERT INTO users (name, email) VALUES (:name, :email)`,
&User{Name: "Jack", Email: "[email protected]"},
)
if err != nil {
tx.Rollback()
return err
}
_, err = tx.Exec(
`INSERT INTO books (title, user_id) VALUES (?, ?)`,
"New Book", 1,
)
if err != nil {
tx.Rollback()
return err
}
return tx.Commit()
GORM vs sqlx 對比
| 維度 | GORM | sqlx |
|---|---|---|
| 抽象程度 | 高(不需要寫 SQL) | 低(你寫 SQL,它幫你掃描) |
| 學習曲線 | 需要學 GORM API | 會 SQL 就能用 |
| CRUD 速度 | 很快(一行搞定) | 中等(要寫 SQL) |
| 複雜查詢 | 困難(易掉入 N+1) | 簡單(就是寫 SQL) |
| 效能 | 有反射開銷 | 接近原生 database/sql |
| Migration | 內建 AutoMigrate | 需搭配其他工具 |
| 關聯查詢 | 內建 Preload | 自行 JOIN |
| Debug | 需要看產生的 SQL | 所見即所得 |
如何選擇?
選 GORM 如果:
- 快速開發 CRUD 為主的應用
- 需要 AutoMigrate 和軟刪除等功能
- 團隊 SQL 經驗較少
選 sqlx 如果:
- 需要精確控制 SQL
- 效能敏感的場景
- 複雜的查詢需求(多表 JOIN、子查詢)
- 偏好「SQL 是一等公民」的哲學
混合使用也是一種策略: 用 GORM 做快速的 CRUD,用 sqlx 處理複雜的報表查詢。
下一步
有了 ORM 工具,下一步要升級資料庫。下一篇我們從 SQLite 切換到 PostgreSQL,學習連線池、Migration、Index 和 JSONB 等進階功能。