Appearance
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 的最佳实践与架构模式:
- Repository 模式:封装数据访问,业务层依赖接口而非实现
- 单元测试:通过 Mock 接口或 SQLite 内存数据库进行测试
- 多数据源管理:DBManager 统一管理多个数据库连接
- 读写分离:使用
dbresolver插件,写走主库,读走从库 - 分库分表:按 hash/range 分散数据,缓解单库压力
- 乐观锁:version 字段 + 条件更新,避免并发冲突
- 防止 SQL 注入:使用参数化查询,避免字符串拼接
- 枚举类型:自定义类型 + String 方法
- JSON 字段:自定义类型实现 Scanner/Valuer 接口
- 时间类型:注意时区设置,使用 time.Time
- 分层架构: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 后端项目的数据库开发需求。在生产实践中,始终记住:性能优先用预加载、安全优先用参数化、可维护优先用分层架构、生产迁移优先用专业工具。