Golang 資料庫操作 — ORM 工具(GORM、sqlx)
簡介
在 Go 語言的後端開發中,資料庫存取是最常見的需求之一。直接使用 database/sql 雖然能提供最底層的控制權,但每一次手寫 SQL、映射結果、處理錯誤都會讓程式碼變得冗長且易錯。為了提升開發效率、降低重複性工作,社群提供了多種 ORM(Object‑Relational Mapping) 工具,其中最受歡迎的兩個是 GORM 與 sqlx。
- GORM:全功能的 ORM,支援模型定義、關聯、遷移(migration)以及自動化的 CRUD 操作。適合希望以「物件」方式操作資料庫的開發者。
- sqlx:在
database/sql之上提供了更便利的結構體映射與命名參數支援,保持了原生 SQL 的靈活性,同時減少了大量樣板程式碼。適合需要 SQL 可見性、又不想放棄結構化映射的情境。
本篇文章將從概念、實作範例、常見陷阱與最佳實踐,帶你快速掌握這兩個工具的使用方式,並說明它們在實務專案中的適用情境。
核心概念
1. 為什麼需要 ORM?
- 降低重複程式:自動產生
INSERT、UPDATE、SELECT等語句。 - 型別安全:Go 的結構體與資料表欄位直接對應,編譯期即可捕捉錯誤。
- 維護成本:資料模型變更時,只需要調整結構體或遷移腳本,減少手動 SQL 的同步工作。
注意:ORM 並非萬能,過度抽象可能會犧牲效能或失去對複雜查詢的掌控。選擇時需衡量 易用性 vs. 可控性。
2. GORM 基礎
2.1 安裝與初始化
// go.mod
module example.com/gormdemo
require (
gorm.io/driver/mysql v1.5.0
gorm.io/gorm v1.25.0
)
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"log"
)
func main() {
dsn := "user:password@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("connect db failed: %v", err)
}
// 自動遷移(建立表格)
db.AutoMigrate(&User{})
}
2.2 定義模型(Model)
type User struct {
ID uint `gorm:"primaryKey"` // 主鍵
Name string `gorm:"size:100;not null"` // 欄位長度與 NOT NULL
Email string `gorm:"uniqueIndex"` // 唯一索引
Age int `gorm:"default:0"` // 預設值
CreatedAt time.Time
UpdatedAt time.Time
}
2.3 基本 CRUD
// 建立
newUser := User{Name: "Alice", Email: "alice@example.com", Age: 28}
if err := db.Create(&newUser).Error; err != nil {
log.Printf("create user error: %v", err)
}
// 讀取(單筆)
var user User
if err := db.First(&user, "email = ?", "alice@example.com").Error; err != nil {
log.Printf("find user error: %v", err)
}
// 更新
db.Model(&user).Update("age", 29)
// 刪除
db.Delete(&User{}, user.ID)
2.4 關聯(Association)
type Profile struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"uniqueIndex"` // 一對一關聯
Bio string
User User `gorm:"foreignKey:UserID"` // 反向關聯
}
// 建立關聯資料
profile := Profile{UserID: newUser.ID, Bio: "Go developer"}
db.Create(&profile)
// 讀取時自動 Preload
var u User
db.Preload("Profile").First(&u, newUser.ID)
fmt.Printf("User: %s, Bio: %s\n", u.Name, u.Profile.Bio)
3. sqlx 基礎
3.1 安裝與初始化
// go.mod
module example.com/sqlxdemo
require (
github.com/jmoiron/sqlx v1.3.5
github.com/go-sql-driver/mysql v1.7.1
)
package main
import (
"github.com/jmoiron/sqlx"
_ "github.com/go-sql-driver/mysql"
"log"
)
func main() {
dsn := "user:password@tcp(127.0.0.1:3306)/testdb?parseTime=true"
db, err := sqlx.Connect("mysql", dsn)
if err != nil {
log.Fatalf("connect db failed: %v", err)
}
defer db.Close()
}
3.2 結構體映射與命名參數
type User struct {
ID int64 `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
Age int `db:"age"`
}
// INSERT 使用命名參數
stmt := `INSERT INTO users (name, email, age) VALUES (:name, :email, :age)`
user := User{Name: "Bob", Email: "bob@example.com", Age: 35}
res, err := db.NamedExec(stmt, &user)
if err != nil {
log.Fatalf("insert error: %v", err)
}
id, _ := res.LastInsertId()
log.Printf("new user id: %d", id)
3.3 查詢與掃描
// 單筆查詢
var u User
err := db.Get(&u, "SELECT * FROM users WHERE email = ?", "bob@example.com")
if err != nil {
log.Printf("get user error: %v", err)
}
// 多筆查詢
var users []User
err = db.Select(&users, "SELECT * FROM users WHERE age > ?", 20)
if err != nil {
log.Printf("select users error: %v", err)
}
3.4 Transaction 範例
tx := db.MustBegin()
tx.NamedExec(`INSERT INTO users (name,email,age) VALUES (:name,:email,:age)`, &User{Name:"Cathy", Email:"cathy@example.com", Age:27})
tx.NamedExec(`INSERT INTO profiles (user_id,bio) VALUES (:uid,:bio)`, map[string]interface{}{
"uid": tx.LastInsertId(),
"bio": "Full‑stack engineer",
})
if err := tx.Commit(); err != nil {
tx.Rollback()
log.Fatalf("transaction failed: %v", err)
}
4. GORM vs. sqlx:何時選哪個?
| 需求 | 建議工具 | 理由 |
|---|---|---|
| 完全物件導向、需要自動遷移 | GORM | 提供 AutoMigrate、關聯、Hook 等高階功能 |
| 複雜查詢、想保留原生 SQL 可讀性 | sqlx | 直接寫 SQL,仍能利用結構體映射 |
| 需要大量批次寫入且效能敏感 | sqlx(配合 Exec、Prepare) |
較少抽象層,執行效率更高 |
| 想快速開發原型、對資料模型變動頻繁 | GORM | 只要改模型結構,遷移腳本自動生成 |
常見陷阱與最佳實踐
遺忘
AutoMigrate或手動遷移- 陷阱:表格結構與程式碼不同步,導致 runtime error。
- 最佳實踐:在 CI/CD pipeline 中加入遷移腳本,或在開發環境啟動時自動呼叫
db.AutoMigrate(&Model{})。
使用 GORM 的
Select("*")造成 N+1 問題- 陷阱:關聯資料未使用
Preload,每筆主資料都會額外發起一次查詢。 - 最佳實踐:根據需求一次性
Preload必要關聯,或改用Joins手寫 SQL。
- 陷阱:關聯資料未使用
sqlx 的
NamedExec參數對應錯誤- 陷阱:結構體欄位名稱與
db標籤不一致,導致參數未被替換。 - 最佳實踐:統一使用
db:"column_name"標籤,並在 IDE 中啟用檢查。
- 陷阱:結構體欄位名稱與
Transaction 沒有正確
Rollback- 陷阱:在錯誤發生後忘記回滾,資料庫留下不一致狀態。
- 最佳實踐:使用
defer func(){ if p := recover(); p != nil { tx.Rollback() } }()包裹整段交易,確保任何 panic 都會回滾。
忽略錯誤處理
- 陷阱:只檢查
err != nil卻未記錄或回傳,難以追蹤問題。 - 最佳實踐:統一使用
log、zap或自訂的錯誤包裝,並在 API 層返回適當的 HTTP 狀態碼。
- 陷阱:只檢查
實際應用場景
1. 企業內部系統(CRUD 為主)
使用 GORM 能快速建立 CRUD 介面,配合 AutoMigrate 讓資料表隨模型演進。開發者只需要關注模型與業務邏輯,減少 SQL 的撰寫時間。
2. 報表系統與資料分析
報表往往需要 複雜聚合、子查詢,此時 sqlx 的原生 SQL 能提供更好的可讀性與效能。透過 sqlx.Select 把結果映射到自訂結構,既保留了型別安全,又不失彈性。
3. 微服務間的資料同步
在微服務架構中,常見 事件驅動的資料寫入。使用 sqlx 的 Prepare + Exec 能在高併發情況下保持低延遲;若需要在同一交易內寫入多張表,則可結合 sqlx.BeginTx。
4. 多租戶(SaaS)平台
多租戶系統需要 動態切換資料庫或 schema。gorm.DB.Session(&gorm.Session{NewDB: true}) 可以輕鬆切換連線;而 sqlx 則可以在同一 *sqlx.DB 上使用 db.Rebind 產生符合不同 driver 的占位符。
總結
- GORM 提供了完整的 ORM 功能,適合想以「物件」方式管理資料庫、快速開發 CRUD 為主的專案。
- sqlx 在保留
database/sql的彈性同時,加入了結構體映射與命名參數,讓開發者在寫原生 SQL 時仍能享受型別安全。 - 選擇時,請根據 專案需求、效能考量與團隊熟悉度 來決定使用哪一個工具;兩者也可以在同一個服務中混用,將各自的優勢發揮到最大。
掌握了 GORM 與 sqlx 的核心概念與實作範例後,你就能在 Go 專案中自由切換「抽象」與「控制」之間的平衡,寫出既 簡潔 又 高效 的資料庫程式碼。祝開發順利,期待在你的下一個 Go 專案中看到這兩把利器的身影!