本文 AI 產出,尚未審核

Golang 資料庫操作 — ORM 工具(GORM、sqlx)

簡介

在 Go 語言的後端開發中,資料庫存取是最常見的需求之一。直接使用 database/sql 雖然能提供最底層的控制權,但每一次手寫 SQL、映射結果、處理錯誤都會讓程式碼變得冗長且易錯。為了提升開發效率、降低重複性工作,社群提供了多種 ORM(Object‑Relational Mapping) 工具,其中最受歡迎的兩個是 GORMsqlx

  • GORM:全功能的 ORM,支援模型定義、關聯、遷移(migration)以及自動化的 CRUD 操作。適合希望以「物件」方式操作資料庫的開發者。
  • sqlx:在 database/sql 之上提供了更便利的結構體映射與命名參數支援,保持了原生 SQL 的靈活性,同時減少了大量樣板程式碼。適合需要 SQL 可見性、又不想放棄結構化映射的情境。

本篇文章將從概念、實作範例、常見陷阱與最佳實踐,帶你快速掌握這兩個工具的使用方式,並說明它們在實務專案中的適用情境。


核心概念

1. 為什麼需要 ORM?

  • 降低重複程式:自動產生 INSERTUPDATESELECT 等語句。
  • 型別安全: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(配合 ExecPrepare 較少抽象層,執行效率更高
想快速開發原型、對資料模型變動頻繁 GORM 只要改模型結構,遷移腳本自動生成

常見陷阱與最佳實踐

  1. 遺忘 AutoMigrate 或手動遷移

    • 陷阱:表格結構與程式碼不同步,導致 runtime error。
    • 最佳實踐:在 CI/CD pipeline 中加入遷移腳本,或在開發環境啟動時自動呼叫 db.AutoMigrate(&Model{})
  2. 使用 GORM 的 Select("*") 造成 N+1 問題

    • 陷阱:關聯資料未使用 Preload,每筆主資料都會額外發起一次查詢。
    • 最佳實踐:根據需求一次性 Preload 必要關聯,或改用 Joins 手寫 SQL。
  3. sqlx 的 NamedExec 參數對應錯誤

    • 陷阱:結構體欄位名稱與 db 標籤不一致,導致參數未被替換。
    • 最佳實踐:統一使用 db:"column_name" 標籤,並在 IDE 中啟用檢查。
  4. Transaction 沒有正確 Rollback

    • 陷阱:在錯誤發生後忘記回滾,資料庫留下不一致狀態。
    • 最佳實踐:使用 defer func(){ if p := recover(); p != nil { tx.Rollback() } }() 包裹整段交易,確保任何 panic 都會回滾。
  5. 忽略錯誤處理

    • 陷阱:只檢查 err != nil 卻未記錄或回傳,難以追蹤問題。
    • 最佳實踐:統一使用 logzap 或自訂的錯誤包裝,並在 API 層返回適當的 HTTP 狀態碼。

實際應用場景

1. 企業內部系統(CRUD 為主)

使用 GORM 能快速建立 CRUD 介面,配合 AutoMigrate 讓資料表隨模型演進。開發者只需要關注模型與業務邏輯,減少 SQL 的撰寫時間。

2. 報表系統與資料分析

報表往往需要 複雜聚合、子查詢,此時 sqlx 的原生 SQL 能提供更好的可讀性與效能。透過 sqlx.Select 把結果映射到自訂結構,既保留了型別安全,又不失彈性。

3. 微服務間的資料同步

在微服務架構中,常見 事件驅動的資料寫入。使用 sqlxPrepare + Exec 能在高併發情況下保持低延遲;若需要在同一交易內寫入多張表,則可結合 sqlx.BeginTx

4. 多租戶(SaaS)平台

多租戶系統需要 動態切換資料庫或 schemagorm.DB.Session(&gorm.Session{NewDB: true}) 可以輕鬆切換連線;而 sqlx 則可以在同一 *sqlx.DB 上使用 db.Rebind 產生符合不同 driver 的占位符。


總結

  • GORM 提供了完整的 ORM 功能,適合想以「物件」方式管理資料庫、快速開發 CRUD 為主的專案。
  • sqlx 在保留 database/sql 的彈性同時,加入了結構體映射與命名參數,讓開發者在寫原生 SQL 時仍能享受型別安全。
  • 選擇時,請根據 專案需求、效能考量與團隊熟悉度 來決定使用哪一個工具;兩者也可以在同一個服務中混用,將各自的優勢發揮到最大。

掌握了 GORM 與 sqlx 的核心概念與實作範例後,你就能在 Go 專案中自由切換「抽象」與「控制」之間的平衡,寫出既 簡潔高效 的資料庫程式碼。祝開發順利,期待在你的下一個 Go 專案中看到這兩把利器的身影!