Skip to content

08-GORM 最佳实践与架构模式

掌握 GORM 的 API 只是起点,真正在生产环境中用好它需要合理的架构设计。本篇将介绍 Repository 模式封装、单元测试、多数据源管理、读写分离、分库分表、乐观锁、防止 SQL 注入、枚举与 JSON 字段处理等最佳实践,并通过一个完整的分层架构示例展示企业级项目结构。

Repository 模式封装 GORM

Repository 模式将数据访问逻辑封装在 Repository 接口背后,业务层只依赖接口而非具体实现。这样做的好处是:业务逻辑可测试(Mock)、可替换(换数据库)、关注点分离。

定义 Repository 接口

go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// User 用户模型
type User struct {
	ID        uint           `gorm:"primaryKey" json:"id"`
	Name      string         `gorm:"type:varchar(50);not null" json:"name"`
	Email     string         `gorm:"type:varchar(150);uniqueIndex;not null" json:"email"`
	Age       int            `gorm:"default:0" json:"age"`
	Status    int            `gorm:"default:1" json:"status"`
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

// UserQuery 查询条件
type UserQuery struct {
	Name   string
	Email  string
	Status *int
	Page   int
	Size   int
}

// UserRepository 用户仓储接口
type UserRepository interface {
	Create(ctx context.Context, user *User) error
	GetByID(ctx context.Context, id uint) (*User, error)
	List(ctx context.Context, query UserQuery) ([]User, int64, error)
	Update(ctx context.Context, user *User) error
	Delete(ctx context.Context, id uint) error
}

// gormUserRepository GORM 实现
type gormUserRepository struct {
	db *gorm.DB
}

func NewGormUserRepository(db *gorm.DB) UserRepository {
	return &gormUserRepository{db: db}
}

func (r *gormUserRepository) Create(ctx context.Context, user *User) error {
	return r.db.WithContext(ctx).Create(user).Error
}

func (r *gormUserRepository) GetByID(ctx context.Context, id uint) (*User, error) {
	var user User
	if err := r.db.WithContext(ctx).First(&user, id).Error; err != nil {
		return nil, err
	}
	return &user, nil
}

func (r *gormUserRepository) List(ctx context.Context, query UserQuery) ([]User, int64, error) {
	var users []User
	var total int64

	db := r.db.WithContext(ctx).Model(&User{})
	if query.Name != "" {
		db = db.Where("name LIKE ?", "%"+query.Name+"%")
	}
	if query.Email != "" {
		db = db.Where("email = ?", query.Email)
	}
	if query.Status != nil {
		db = db.Where("status = ?", *query.Status)
	}

	if err := db.Count(&total).Error; err != nil {
		return nil, 0, err
	}

	page := query.Page
	size := query.Size
	if page <= 0 {
		page = 1
	}
	if size <= 0 || size > 100 {
		size = 10
	}
	if err := db.Order("id desc").
		Offset((page - 1) * size).
		Limit(size).
		Find(&users).Error; err != nil {
		return nil, 0, err
	}
	return users, total, nil
}

func (r *gormUserRepository) Update(ctx context.Context, user *User) error {
	return r.db.WithContext(ctx).Save(user).Error
}

func (r *gormUserRepository) Delete(ctx context.Context, id uint) error {
	return r.db.WithContext(ctx).Delete(&User{}, id).Error
}

func main() {
	db, err := gorm.Open(sqlite.Open("repo.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	// 使用 Repository
	repo := NewGormUserRepository(db)
	ctx := context.Background()

	// 创建
	user := &User{Name: "Tom", Email: "tom@x.com", Age: 25}
	repo.Create(ctx, user)
	fmt.Printf("创建用户 ID=%d\n", user.ID)

	// 查询
	found, _ := repo.GetByID(ctx, user.ID)
	fmt.Printf("查询到: %s\n", found.Name)

	// 列表
	status := 1
	users, total, _ := repo.List(ctx, UserQuery{Status: &status, Page: 1, Size: 10})
	fmt.Printf("列表: %d 条 / 总 %d\n", len(users), total)
}

单元测试 Mock

Repository 接口让单元测试变得简单——只需要 Mock 接口即可,无需真实数据库。

go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID        uint           `gorm:"primaryKey" json:"id"`
	Name      string         `gorm:"type:varchar(50);not null" json:"name"`
	Email     string         `gorm:"type:varchar(150);uniqueIndex;not null" json:"email"`
	Age       int            `gorm:"default:0" json:"age"`
	Status    int            `gorm:"default:1" json:"status"`
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

type UserRepository interface {
	Create(ctx context.Context, user *User) error
	GetByID(ctx context.Context, id uint) (*User, error)
}

// mockUserRepository 内存 Mock 实现
type mockUserRepository struct {
	users map[uint]*User
}

func NewMockUserRepository() UserRepository {
	return &mockUserRepository{users: make(map[uint]*User)}
}

func (m *mockUserRepository) Create(ctx context.Context, user *User) error {
	user.ID = uint(len(m.users) + 1)
	m.users[user.ID] = user
	return nil
}

func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*User, error) {
	if u, ok := m.users[id]; ok {
		return u, nil
	}
	return nil, gorm.ErrRecordNotFound
}

// UserService 业务层,依赖 Repository 接口
type UserService struct {
	repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
	return &UserService{repo: repo}
}

func (s *UserService) Register(ctx context.Context, name, email string) (*User, error) {
	user := &User{Name: name, Email: email}
	if err := s.repo.Create(ctx, user); err != nil {
		return nil, err
	}
	return user, nil
}

func main() {
	// 使用 Mock 测试业务逻辑(无需数据库)
	mockRepo := NewMockUserRepository()
	svc := NewUserService(mockRepo)

	ctx := context.Background()
	user, err := svc.Register(ctx, "Tom", "tom@x.com")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Mock 测试: 创建用户 %s (id=%d)\n", user.Name, user.ID)

	// 也可以用真实数据库
	db, err := gorm.Open(sqlite.Open("test_repo.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})
}

单元测试:使用 SQLite 或 Mock

Repository 模式天然支持测试。可以使用 SQLite 内存数据库做集成测试,或用 Mock 做单元测试。

go
package main

import (
	"context"
	"fmt"
	"log"
	"testing"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID        uint           `gorm:"primaryKey" json:"id"`
	Name      string         `gorm:"type:varchar(50);not null" json:"name"`
	Email     string         `gorm:"type:varchar(150);uniqueIndex;not null" json:"email"`
	Age       int            `gorm:"default:0" json:"age"`
	Status    int            `gorm:"default:1" json:"status"`
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

// NewTestDB 创建测试用 SQLite 内存数据库
func NewTestDB() *gorm.DB {
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})
	return db
}

// 测试创建用户
func TestCreateUser(t *testing.T) {
	db := NewTestDB()
	ctx := context.Background()

	user := &User{Name: "Tom", Email: "tom@x.com", Age: 25}
	if err := db.WithContext(ctx).Create(user).Error; err != nil {
		t.Fatalf("创建用户失败: %v", err)
	}
	if user.ID == 0 {
		t.Error("ID 应该被自动填充")
	}

	var found User
	db.First(&found, user.ID)
	if found.Name != "Tom" {
		t.Errorf("期望 Tom, 得到 %s", found.Name)
	}
	fmt.Println("TestCreateUser 通过")
}

// 测试唯一索引
func TestUniqueEmail(t *testing.T) {
	db := NewTestDB()

	db.Create(&User{Name: "A", Email: "dup@x.com"})
	err := db.Create(&User{Name: "B", Email: "dup@x.com"}).Error
	if err == nil {
		t.Error("重复邮箱应该报错")
	}
	fmt.Println("TestUniqueEmail 通过")
}

// 测试软删除
func TestSoftDelete(t *testing.T) {
	db := NewTestDB()

	user := &User{Name: "Tom", Email: "tom@x.com"}
	db.Create(user)
	db.Delete(user)

	var count int64
	db.Model(&User{}).Count(&count)
	if count != 0 {
		t.Error("软删除后应该查不到")
	}

	var allCount int64
	db.Unscoped().Model(&User{}).Count(&allCount)
	if allCount != 1 {
		t.Error("Unscoped 应该查到软删除记录")
	}
	fmt.Println("TestSoftDelete 通过")
}

func main() {
	// 手动运行测试(实际项目用 go test)
	TestCreateUser(&testing.T{})
	TestUniqueEmail(&testing.T{})
	TestSoftDelete(&testing.T{})
	fmt.Println("所有测试通过")
}

多数据源管理

企业级应用常常需要连接多个数据库。可以封装一个 DB 管理器统一管理。

go
package main

import (
	"fmt"
	"log"
	"sync"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// DBManager 多数据源管理器
type DBManager struct {
	dbs  map[string]*gorm.DB
	mu   sync.RWMutex
}

func NewDBManager() *DBManager {
	return &DBManager{dbs: make(map[string]*gorm.DB)}
}

func (m *DBManager) Add(name string, db *gorm.DB) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.dbs[name] = db
}

func (m *DBManager) Get(name string) (*gorm.DB, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	db, ok := m.dbs[name]
	if !ok {
		return nil, fmt.Errorf("数据源 %s 不存在", name)
	}
	return db, nil
}

type User struct {
	ID   uint `gorm:"primaryKey"`
	Name string
}

type Order struct {
	ID     uint `gorm:"primaryKey"`
	Amount float64
}

func main() {
	mgr := NewDBManager()

	// 主库
	mainDB, err := gorm.Open(sqlite.Open("main.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	mainDB.AutoMigrate(&User{})
	mgr.Add("main", mainDB)

	// 订单库
	orderDB, err := gorm.Open(sqlite.Open("order.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	orderDB.AutoMigrate(&Order{})
	mgr.Add("order", orderDB)

	// 使用不同的数据源
	userDB, _ := mgr.Get("main")
	userDB.Create(&User{Name: "Tom"})
	var user User
	userDB.First(&user, 1)
	fmt.Printf("主库用户: %s\n", user.Name)

	ordDB, _ := mgr.Get("order")
	ordDB.Create(&Order{Amount: 100})
	var order Order
	ordDB.First(&order, 1)
	fmt.Printf("订单库金额: %.0f\n", order.Amount)
}

读写分离

GORM 官方提供了 dbresolver 插件实现读写分离:写操作走主库,读操作走从库。

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID   uint `gorm:"primaryKey"`
	Name string
}

func main() {
	// 主库(写)
	mainDB, err := gorm.Open(sqlite.Open("master.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	mainDB.AutoMigrate(&User{})

	// 从库(读)
	replicaDB, err := gorm.Open(sqlite.Open("replica.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	replicaDB.AutoMigrate(&User{})

	// 使用 dbresolver 插件(伪代码,需要 import dbresolver 包)
	// db.Use(
	//     dbresolver.Register(dbresolver.Config{
	//         Sources:      []gorm.Dialector{sqlite.Open("master.db")},  // 写
	//         Replicas:     []gorm.Dialector{sqlite.Open("replica.db")}, // 读
	//         Policy:       dbresolver.RandomPolicy{},
	//     }).
	//     SetMaxOpenConns(100).
	//     SetMaxIdleConns(20),
	// )

	// 手动读写分离示例
	// 写操作:用主库
	mainDB.Create(&User{Name: "Tom"})
	fmt.Println("写入主库成功")

	// 读操作:可以从从库(如果已同步)
	var user User
	mainDB.First(&user, 1)
	fmt.Printf("读取: %s\n", user.Name)

	// 指定使用主库读取(强一致性场景)
	// db.Clauses(dbresolver.Write).First(&user, 1)
	// 指定使用从库读取
	// db.Clauses(dbresolver.Read).First(&user, 1)
}

分库分表策略

数据量巨大时,单库单表无法承载,需要分库分表。常见策略:

  • 水平分表:按 hash、range 等将数据分散到多表
  • 垂直分库:按业务模块拆分到不同数据库
go
package main

import (
	"fmt"
	"hash/fnv"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type Order struct {
	ID     uint `gorm:"primaryKey"`
	UserID uint
	Amount float64
}

// ShardedDB 分表数据库管理
type ShardedDB struct {
	dbs   []*gorm.DB
	shard int
}

func NewShardedDB(shardCount int) *ShardedDB {
	dbs := make([]*gorm.DB, shardCount)
	for i := 0; i < shardCount; i++ {
		db, err := gorm.Open(sqlite.Open(fmt.Sprintf("shard_%d.db", i)), &gorm.Config{})
		if err != nil {
			log.Fatal(err)
		}
		db.AutoMigrate(&Order{})
		dbs[i] = db
	}
	return &ShardedDB{dbs: dbs, shard: shardCount}
}

// getShard 根据 userID 计算分片
func (s *ShardedDB) getShard(userID uint) *gorm.DB {
	h := fnv.New32a()
	h.Write([]byte(fmt.Sprintf("%d", userID)))
	return s.dbs[h.Sum32()%uint32(s.shard)]
}

func (s *ShardedDB) Create(order *Order) error {
	db := s.getShard(order.UserID)
	return db.Create(order).Error
}

func (s *ShardedDB) GetByUserID(userID uint) ([]Order, error) {
	db := s.getShard(userID)
	var orders []Order
	return orders, db.Where("user_id = ?", userID).Find(&orders).Error
}

func main() {
	shardedDB := NewShardedDB(4)

	// 创建不同用户的订单(会分散到不同分片)
	for i := 1; i <= 10; i++ {
		order := &Order{UserID: uint(i), Amount: float64(i * 100)}
		shardedDB.Create(order)
		fmt.Printf("用户 %d 的订单创建到分片\n", i)
	}

	// 查询某用户的订单
	orders, _ := shardedDB.GetByUserID(5)
	fmt.Printf("用户 5 的订单数: %d\n", len(orders))
}

乐观锁实现:version 字段

乐观锁通过版本号机制避免并发冲突:更新时检查版本号,版本不匹配则更新失败。

go
package main

import (
	"errors"
	"fmt"
	"log"
	"sync"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type Product struct {
	ID      uint `gorm:"primaryKey"`
	Name    string
	Stock   int
	Version int `gorm:"default:0"` // 乐观锁版本号
}

// UpdateStockWithOptimisticLock 乐观锁更新库存
func UpdateStockWithOptimisticLock(db *gorm.DB, id uint, quantity int) error {
	// 1. 查询当前版本
	var product Product
	if err := db.First(&product, id).Error; err != nil {
		return err
	}

	if product.Stock < quantity {
		return errors.New("库存不足")
	}

	// 2. 更新时检查版本号
	result := db.Model(&Product{}).
		Where("id = ? AND version = ?", id, product.Version).
		Updates(map[string]interface{}{
			"stock":   product.Stock - quantity,
			"version": product.Version + 1,
		})

	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return errors.New("并发冲突,请重试")
	}
	return nil
}

func main() {
	db, err := gorm.Open(sqlite.Open("optimistic.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&Product{})

	product := Product{Name: "iPhone", Stock: 10}
	db.Create(&product)

	// 模拟并发扣减库存
	var wg sync.WaitGroup
	successCount := 0
	var mu sync.Mutex

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			err := UpdateStockWithOptimisticLock(db, product.ID, 3)
			if err == nil {
				mu.Lock()
				successCount++
				mu.Unlock()
			}
		}()
	}
	wg.Wait()

	// 只有前 3 次能成功(10/3=3余1)
	var final Product
	db.First(&final, product.ID)
	fmt.Printf("成功扣减次数: %d, 剩余库存: %d, 版本: %d\n",
		successCount, final.Stock, final.Version)
}

防止 SQL 注入

GORM 默认使用参数化查询,能有效防止 SQL 注入。但使用原生 SQL 或字符串拼接时要小心。

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID   uint `gorm:"primaryKey"`
	Name string
	City string
}

func main() {
	db, err := gorm.Open(sqlite.Open("injection.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	db.Create(&User{Name: "Tom", City: "Beijing"})
	db.Create(&User{Name: "Jerry", City: "Shanghai"})

	// 危险输入(模拟 SQL 注入)
	malicious := "Beijing'; DROP TABLE users; --"

	// 安全:GORM 自动参数化,不会执行 DROP TABLE
	var users []User
	db.Where("city = ?", malicious).Find(&users)
	fmt.Printf("安全查询: %d 条(无注入)\n", len(users))

	// 表依然存在
	var count int64
	db.Model(&User{}).Count(&count)
	fmt.Printf("表未受损,共 %d\n", count)

	// 危险:字符串拼接(不要这样做!)
	// db.Raw("SELECT * FROM users WHERE city = '" + malicious + "'").Scan(&users)

	// 安全:使用占位符
	db.Raw("SELECT * FROM users WHERE city = ?", malicious).Scan(&users)
	fmt.Printf("Raw 占位符查询: %d\n", len(users))

	// 安全:使用命名参数
	db.Raw("SELECT * FROM users WHERE city = @city",
		map[string]interface{}{"city": "Beijing"}).Scan(&users)
	fmt.Printf("命名参数查询: %d\n", len(users))
}

枚举类型处理

Go 没有内置枚举,但可以通过自定义类型实现。

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// OrderStatus 订单状态枚举
type OrderStatus int

const (
	StatusPending   OrderStatus = 0 // 待支付
	StatusPaid      OrderStatus = 1 // 已支付
	StatusShipped   OrderStatus = 2 // 已发货
	StatusCompleted OrderStatus = 3 // 已完成
	StatusCancelled OrderStatus = 4 // 已取消
)

func (s OrderStatus) String() string {
	switch s {
	case StatusPending:
		return "待支付"
	case StatusPaid:
		return "已支付"
	case StatusShipped:
		return "已发货"
	case StatusCompleted:
		return "已完成"
	case StatusCancelled:
		return "已取消"
	default:
		return "未知"
	}
}

// Order 订单
type Order struct {
	ID     uint        `gorm:"primaryKey"`
	Amount float64
	Status OrderStatus `gorm:"default:0"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("enum.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&Order{})

	// 创建订单(默认状态)
	order := Order{Amount: 100}
	db.Create(&order)
	fmt.Printf("创建: %s\n", order.Status.String())

	// 更新状态
	db.Model(&order).Update("status", StatusPaid)
	db.First(&order, order.ID)
	fmt.Printf("支付后: %s\n", order.Status.String())

	// 按状态查询
	db.Model(&order).Update("status", StatusShipped)
	var paidOrders []Order
	db.Where("status = ?", StatusShipped).Find(&paidOrders)
	fmt.Printf("已发货订单: %d\n", len(paidOrders))
}

JSON 字段处理

MySQL 5.7+、PostgreSQL、SQLite 3.38+ 都支持 JSON 字段类型。

go
package main

import (
	"encoding/json"
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// Product 商品,包含 JSON 类型的属性
type Product struct {
	ID         uint `gorm:"primaryKey"`
	Name       string
	Attributes datatypes.JSON `gorm:"type:json"` // 需要 gorm.io/datatypes 包
}

// 简化版:实际项目中用 gorm.io/datatypes.JSON
type datatypesJSON struct {
	data []byte
}

func main() {
	db, err := gorm.Open(sqlite.Open("json.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}

	// 方式1:使用 string/[]byte 存 JSON
	type ProductSimple struct {
		ID         uint `gorm:"primaryKey"`
		Name       string
		Attributes string `gorm:"type:text"`
	}
	db.AutoMigrate(&ProductSimple{})

	attrs, _ := json.Marshal(map[string]interface{}{
		"color": "red",
		"size":  "L",
		"tags":  []string{"new", "hot"},
	})
	p := ProductSimple{Name: "T-shirt", Attributes: string(attrs)}
	db.Create(&p)

	// 读取并解析
	var found ProductSimple
	db.First(&found, p.ID)
	var attrMap map[string]interface{}
	json.Unmarshal([]byte(found.Attributes), &attrMap)
	fmt.Printf("颜色: %v, 尺寸: %v\n", attrMap["color"], attrMap["size"])

	// 方式2:使用自定义类型实现 Scanner/Valuer
	type ProductWithAttrs struct {
		ID   uint `gorm:"primaryKey"`
		Name string
		Attrs Attrs `gorm:"type:text"`
	}
	db.AutoMigrate(&ProductWithAttrs{})

	p2 := ProductWithAttrs{
		Name: "Phone",
		Attrs: Attrs{Color: "black", Weight: 200},
	}
	db.Create(&p2)

	var found2 ProductWithAttrs
	db.First(&found2, p2.ID)
	fmt.Printf("手机: %s, 颜色 %s, 重量 %dg\n",
		found2.Name, found2.Attrs.Color, found2.Attrs.Weight)
}

// Attrs 自定义 JSON 类型
type Attrs struct {
	Color  string `json:"color"`
	Weight int    `json:"weight"`
}

// 实现 driver.Valuer 接口:写入数据库
func (a Attrs) Value() (interface{}, error) {
	return json.Marshal(a)
}

// 实现 sql.Scanner 接口:从数据库读取
func (a *Attrs) Scan(value interface{}) error {
	bytes, ok := value.([]byte)
	if !ok {
		return fmt.Errorf("无法扫描到 Attrs")
	}
	return json.Unmarshal(bytes, a)
}

// 为前面的 Product 定义占位(实际使用 datatypes.JSON)
type datatypes struct{ JSON }
type JSON []byte

func (j JSON) Value() (interface{}, error)  { return json.Marshal(j) }
func (j *JSON) Scan(value interface{}) error {
	b, _ := value.([]byte)
	*j = JSON(b)
	return nil
}

时间类型处理

GORM 自动处理 time.Time,但需要注意时区。

go
package main

import (
	"fmt"
	"log"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type Event struct {
	ID        uint      `gorm:"primaryKey"`
	Name      string
	StartTime time.Time `gorm:"type:datetime"`
	EndTime   time.Time `gorm:"type:datetime"`
	// Unix 时间戳(秒)
	Timestamp int64 `gorm:"autoCreateTime"`
	// 自定义时区字段
	CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("time.db"), &gorm.Config{
		// 全局时区设置(MySQL DSN 中通过 loc=Local 设置)
	})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&Event{})

	// 加载本地时区
	loc, _ := time.LoadLocation("Asia/Shanghai")

	event := Event{
		Name:      "会议",
		StartTime: time.Date(2024, 12, 1, 10, 0, 0, 0, loc),
		EndTime:   time.Date(2024, 12, 1, 12, 0, 0, 0, loc),
	}
	db.Create(&event)

	var found Event
	db.First(&found, event.ID)
	fmt.Printf("事件: %s\n", found.Name)
	fmt.Printf("开始: %s\n", found.StartTime.In(loc).Format("2006-01-02 15:04:05"))
	fmt.Printf("结束: %s\n", found.EndTime.In(loc).Format("2006-01-02 15:04:05"))
	fmt.Printf("时间戳: %d\n", found.Timestamp)

	// 按时间范围查询
	now := time.Now()
	var upcoming []Event
	db.Where("start_time > ?", now.AddDate(-1, 0, 0)).Find(&upcoming)
	fmt.Printf("未来事件数: %d\n", len(upcoming))
}

完整示例:分层架构项目结构

下面展示一个完整的分层架构项目结构,包含 Handler -> Service -> Repository -> Model 各层。

项目目录结构

myapp/
├── cmd/
│   └── server/
│       └── main.go          # 程序入口
├── internal/
│   ├── config/              # 配置
│   │   └── config.go
│   ├── model/               # 数据模型
│   │   └── user.go
│   ├── repository/          # 数据访问层
│   │   └── user_repo.go
│   ├── service/             # 业务逻辑层
│   │   └── user_service.go
│   └── handler/             # HTTP 处理层
│       └── user_handler.go
├── pkg/
│   └── database/            # 数据库初始化
│       └── db.go
├── go.mod
└── go.sum

配置加载

go
package main

import (
	"fmt"
	"log"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// Config 配置结构
type Config struct {
	DB DBConfig
}

type DBConfig struct {
	Driver          string
	DSN             string
	MaxIdleConns    int
	MaxOpenConns    int
	ConnMaxLifetime time.Duration
	LogLevel        string
}

// LoadConfig 加载配置(实际项目从 yaml/env 读取)
func LoadConfig() *Config {
	return &Config{
		DB: DBConfig{
			Driver:          "sqlite",
			DSN:             "app.db",
			MaxIdleConns:    10,
			MaxOpenConns:    100,
			ConnMaxLifetime: time.Hour,
			LogLevel:        "warn",
		},
	}
}

// NewDB 创建数据库连接
func NewDB(cfg *DBConfig) (*gorm.DB, error) {
	var db *gorm.DB
	var err error

	switch cfg.Driver {
	case "sqlite":
		db, err = gorm.Open(sqlite.Open(cfg.DSN), &gorm.Config{
			Logger: logger.Default.LogMode(logger.Warn),
		})
	case "mysql":
		// db, err = gorm.Open(mysql.Open(cfg.DSN), &gorm.Config{})
		err = fmt.Errorf("MySQL 驱动未启用")
	default:
		err = fmt.Errorf("不支持的驱动: %s", cfg.Driver)
	}
	if err != nil {
		return nil, err
	}

	sqlDB, _ := db.DB()
	sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
	sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
	sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
	return db, nil
}

// User 模型
type User struct {
	ID        uint           `gorm:"primaryKey" json:"id"`
	Name      string         `gorm:"type:varchar(50);not null" json:"name"`
	Email     string         `gorm:"type:varchar(150);uniqueIndex;not null" json:"email"`
	Age       int            `gorm:"default:0" json:"age"`
	Status    int            `gorm:"default:1" json:"status"`
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

// ===== Repository 层 =====

type UserRepository interface {
	Create(user *User) error
	GetByID(id uint) (*User, error)
	List(page, size int) ([]User, int64, error)
	Update(user *User) error
	Delete(id uint) error
}

type userRepository struct {
	db *gorm.DB
}

func NewUserRepository(db *gorm.DB) UserRepository {
	return &userRepository{db: db}
}

func (r *userRepository) Create(user *User) error {
	return r.db.Create(user).Error
}

func (r *userRepository) GetByID(id uint) (*User, error) {
	var user User
	if err := r.db.First(&user, id).Error; err != nil {
		return nil, err
	}
	return &user, nil
}

func (r *userRepository) List(page, size int) ([]User, int64, error) {
	var users []User
	var total int64
	r.db.Model(&User{}).Count(&total)
	if page <= 0 {
		page = 1
	}
	if size <= 0 || size > 100 {
		size = 10
	}
	err := r.db.Order("id desc").
		Offset((page - 1) * size).
		Limit(size).
		Find(&users).Error
	return users, total, err
}

func (r *userRepository) Update(user *User) error {
	return r.db.Save(user).Error
}

func (r *userRepository) Delete(id uint) error {
	return r.db.Delete(&User{}, id).Error
}

// ===== Service 层 =====

type UserService struct {
	repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
	return &UserService{repo: repo}
}

func (s *UserService) Register(name, email string, age int) (*User, error) {
	if name == "" {
		return nil, fmt.Errorf("用户名不能为空")
	}
	user := &User{Name: name, Email: email, Age: age}
	if err := s.repo.Create(user); err != nil {
		return nil, fmt.Errorf("注册失败: %w", err)
	}
	return user, nil
}

func (s *UserService) GetProfile(id uint) (*User, error) {
	user, err := s.repo.GetByID(id)
	if err != nil {
		return nil, fmt.Errorf("用户不存在: %w", err)
	}
	return user, nil
}

func (s *UserService) ListUsers(page, size int) ([]User, int64, error) {
	return s.repo.List(page, size)
}

// ===== Handler 层 =====

type UserHandler struct {
	svc *UserService
}

func NewUserHandler(svc *UserService) *UserHandler {
	return &UserHandler{svc: svc}
}

// RegisterHandler 注册用户(实际项目中是 HTTP handler)
func (h *UserHandler) RegisterHandler(name, email string, age int) (uint, error) {
	user, err := h.svc.Register(name, email, age)
	if err != nil {
		return 0, err
	}
	return user.ID, nil
}

// ListHandler 列表查询
func (h *UserHandler) ListHandler(page, size int) ([]User, int64, error) {
	return h.svc.ListUsers(page, size)
}

func main() {
	// 1. 加载配置
	cfg := LoadConfig()

	// 2. 初始化数据库
	db, err := NewDB(&cfg.DB)
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	// 3. 依赖注入:Repository -> Service -> Handler
	userRepo := NewUserRepository(db)
	userSvc := NewUserService(userRepo)
	userHandler := NewUserHandler(userSvc)

	// 4. 使用
	id, err := userHandler.RegisterHandler("Tom", "tom@x.com", 25)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("注册用户 ID=%d\n", id)

	id2, _ := userHandler.RegisterHandler("Jerry", "jerry@x.com", 30)
	fmt.Printf("注册用户 ID=%d\n", id2)

	users, total, _ := userHandler.ListHandler(1, 10)
	fmt.Printf("用户列表: 共 %d 条, 当前页 %d\n", total, len(users))
	for _, u := range users {
		fmt.Printf("  - %s (%s)\n", u.Name, u.Email)
	}
}

小结

本篇系统讲解了 GORM 的最佳实践与架构模式:

  1. Repository 模式:封装数据访问,业务层依赖接口而非实现
  2. 单元测试:通过 Mock 接口或 SQLite 内存数据库进行测试
  3. 多数据源管理:DBManager 统一管理多个数据库连接
  4. 读写分离:使用 dbresolver 插件,写走主库,读走从库
  5. 分库分表:按 hash/range 分散数据,缓解单库压力
  6. 乐观锁:version 字段 + 条件更新,避免并发冲突
  7. 防止 SQL 注入:使用参数化查询,避免字符串拼接
  8. 枚举类型:自定义类型 + String 方法
  9. JSON 字段:自定义类型实现 Scanner/Valuer 接口
  10. 时间类型:注意时区设置,使用 time.Time
  11. 分层架构:Handler -> Service -> Repository -> Model,依赖注入组装

GORM 系列总结

至此,GORM 深度教程 8 篇全部完成:

  • 01 模型定义:安装、连接、模型、标签、AutoMigrate
  • 02 CRUD 基础:增删改查、查询条件、聚合、软删除
  • 03 关联关系:一对一、一对多、多对多、预加载、多态
  • 04 事务与钩子:事务、嵌套事务、生命周期钩子
  • 05 查询进阶:Scope、子查询、原生 SQL、JOIN、窗口函数
  • 06 迁移与索引:Migrator API、索引管理、golang-migrate
  • 07 性能优化:预加载、批量操作、连接池、日志、缓存
  • 08 最佳实践:Repository 模式、测试、多数据源、分层架构

掌握这些内容,足以应对绝大多数 Go 后端项目的数据库开发需求。在生产实践中,始终记住:性能优先用预加载、安全优先用参数化、可维护优先用分层架构、生产迁移优先用专业工具。