Skip to content

GORM 使用规范

核心原则

GORM Model 仅用于数据映射,不管理表结构。所有数据库结构通过独立的迁移工具实现。

允许的做法

Model 定义

  • 使用 column 标签指定列名
  • 使用 指针类型表示可空字段
  • 使用 int64 存储时间戳
go
// ✅ 正确 - Model 定义
type User struct {
    ID          int64   `json:"id" gorm:"column:id"`
    Name        string  `json:"name" gorm:"column:name"`
    Email       string  `json:"email" gorm:"column:email"`
    Password    string  `json:"-" gorm:"column:password"`        // json:"-" 不返回
    Phone       *string `json:"phone" gorm:"column:phone"`       // 可空字段用指针
    Status      string  `json:"status" gorm:"column:status"`
    CreatedAt   int64   `json:"created_at" gorm:"column:created_at"`
    UpdatedAt   int64   `json:"updated_at" gorm:"column:updated_at"`
    DeletedAt   *int64  `json:"deleted_at" gorm:"column:deleted_at"`
}

func (User) TableName() string {
    return "users"
}

字段类型映射

MySQL 类型Go 类型说明
BIGINTint64主键、外键、时间戳
INTint / int32普通整数
VARCHARstring字符串
TEXTstring长文本
JSONstringJSON 数据
DECIMALfloat64精确小数
TINYINT(1)bool布尔值

查询操作

go
// ✅ 正确 - 基本查询
func (s *UserService) FindByID(ctx context.Context, id int64) (*models.User, error) {
    var user models.User
    err := s.db.WithContext(ctx).
        Where("id = ? AND deleted_at IS NULL", id).
        First(&user).Error

    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, errors.New("用户不存在")
        }
        return nil, fmt.Errorf("查询失败: %w", err)
    }

    return &user, nil
}

// ✅ 正确 - 条件查询
func (s *UserService) List(ctx context.Context, status string, page, pageSize int) ([]models.User, int64, error) {
    var users []models.User
    var total int64

    query := s.db.WithContext(ctx).Model(&models.User{}).
        Where("deleted_at IS NULL")

    if status != "" {
        query = query.Where("status = ?", status)
    }

    // 查询总数
    if err := query.Count(&total).Error; err != nil {
        return nil, 0, err
    }

    // 分页查询
    offset := (page - 1) * pageSize
    if err := query.Offset(offset).Limit(pageSize).Find(&users).Error; err != nil {
        return nil, 0, err
    }

    return users, total, nil
}

创建和更新

go
// ✅ 正确 - 创建
func (s *UserService) Create(ctx context.Context, user *models.User) error {
    user.CreatedAt = time.Now().Unix()
    user.UpdatedAt = time.Now().Unix()

    if err := s.db.WithContext(ctx).Create(user).Error; err != nil {
        return fmt.Errorf("创建失败: %w", err)
    }

    return nil
}

// ✅ 正确 - 更新
func (s *UserService) Update(ctx context.Context, id int64, updates map[string]interface{}) error {
    updates["updated_at"] = time.Now().Unix()

    result := s.db.WithContext(ctx).
        Model(&models.User{}).
        Where("id = ? AND deleted_at IS NULL", id).
        Updates(updates)

    if result.Error != nil {
        return fmt.Errorf("更新失败: %w", result.Error)
    }

    if result.RowsAffected == 0 {
        return errors.New("用户不存在或已删除")
    }

    return nil
}

// ✅ 正确 - 软删除
func (s *UserService) Delete(ctx context.Context, id int64) error {
    now := time.Now().Unix()
    result := s.db.WithContext(ctx).
        Model(&models.User{}).
        Where("id = ? AND deleted_at IS NULL", id).
        Update("deleted_at", now)

    if result.Error != nil {
        return fmt.Errorf("删除失败: %w", result.Error)
    }

    if result.RowsAffected == 0 {
        return errors.New("用户不存在或已删除")
    }

    return nil
}

事务处理

go
// ✅ 正确 - 事务
func (s *OrderService) Create(ctx context.Context, req *CreateOrderRequest) (*models.Order, error) {
    var order *models.Order

    err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        // 创建订单
        order = &models.Order{
            UserID:      req.UserID,
            TotalAmount: req.TotalAmount,
            Status:      "pending",
            CreatedAt:   time.Now().Unix(),
            UpdatedAt:   time.Now().Unix(),
        }

        if err := tx.Create(order).Error; err != nil {
            return fmt.Errorf("创建订单失败: %w", err)
        }

        // 创建订单项
        for _, item := range req.Items {
            orderItem := &models.OrderItem{
                OrderID:    order.ID,
                ProductID:  item.ProductID,
                Quantity:   item.Quantity,
                CreatedAt:  time.Now().Unix(),
            }

            if err := tx.Create(orderItem).Error; err != nil {
                return fmt.Errorf("创建订单项失败: %w", err)
            }
        }

        return nil
    })

    return order, err
}

批量操作

go
// ✅ 正确 - 批量插入
func (s *ProductService) BatchCreate(ctx context.Context, products []models.Product) error {
    return s.db.WithContext(ctx).CreateInBatches(products, 100).Error
}

// ✅ 正确 - IN 查询
func (s *UserService) FindByIDs(ctx context.Context, ids []int64) ([]models.User, error) {
    var users []models.User
    err := s.db.WithContext(ctx).
        Where("id IN ? AND deleted_at IS NULL", ids).
        Find(&users).Error

    return users, err
}

SQL 注入防护

核心原则必须使用参数化查询,禁止拼接 SQL 字符串。

安全风险:SQL 注入

SQL 注入是 OWASP Top 10 安全风险之一。当直接拼接用户输入到 SQL 语句时,攻击者可以注入恶意 SQL 代码,导致数据泄露、篡改或删除。

正确做法:参数化查询

使用 GORM 的 Where() 占位符 ?,让 GORM 自动转义参数:

go
// ✅ 正确 - 使用占位符
func (s *UserService) FindByEmail(ctx context.Context, email string) (*models.User, error) {
    var user models.User
    // GORM 会自动转义 email 参数,防止 SQL 注入
    err := s.db.WithContext(ctx).
        Where("email = ? AND deleted_at IS NULL", email).
        First(&user).Error

    return &user, err
}

// ✅ 正确 - 多个占位符
func (s *UserService) Search(ctx context.Context, name, status string) ([]models.User, error) {
    var users []models.User
    err := s.db.WithContext(ctx).
        Where("name LIKE ? AND status = ? AND deleted_at IS NULL", "%"+name+"%", status).
        Find(&users).Error

    return users, err
}

// ✅ 正确 - 命名占位符(map 形式)
func (s *ProductService) FindByConditions(ctx context.Context, category string, minPrice float64) ([]models.Product, error) {
    var products []models.Product
    err := s.db.WithContext(ctx).
        Where("category = @category AND price >= @minPrice", map[string]interface{}{
            "category": category,
            "minPrice": minPrice,
        }).
        Find(&products).Error

    return products, err
}

// ✅ 正确 - 结构体条件(自动参数化)
func (s *UserService) FindByStatus(ctx context.Context, status string) ([]models.User, error) {
    var users []models.User
    err := s.db.WithContext(ctx).
        Where(&models.User{Status: status}).
        Find(&users).Error

    return users, err
}

错误做法:字符串拼接

禁止直接拼接用户输入到 SQL 字符串:

go
// ❌ 错误 - 字符串拼接(存在 SQL 注入风险)
func (s *UserService) FindByEmailUnsafe(ctx context.Context, email string) (*models.User, error) {
    var user models.User
    // 危险:如果 email = "' OR '1'='1",会返回所有用户
    sql := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
    err := s.db.WithContext(ctx).Raw(sql).Scan(&user).Error
    return &user, err
}

// ❌ 错误 - 动态表名拼接
func (s *UserService) FindFromTableUnsafe(ctx context.Context, tableName string) ([]models.User, error) {
    var users []models.User
    // 危险:tableName 可能被注入为 "users; DROP TABLE users;--"
    err := s.db.WithContext(ctx).Table(tableName).Find(&users).Error
    return users, err
}

// ❌ 错误 - 使用 fmt.Sprintf 构造 WHERE 条件
func (s *ProductService) SearchUnsafe(ctx context.Context, keyword string) ([]models.Product, error) {
    var products []models.Product
    // 危险:keyword 未经转义
    condition := fmt.Sprintf("name LIKE '%%%s%%'", keyword)
    err := s.db.WithContext(ctx).Where(condition).Find(&products).Error
    return products, err
}

特殊场景:动态列名和表名

当需要动态列名或表名时,必须使用白名单验证:

go
// ✅ 正确 - 白名单验证动态列名
func (s *UserService) OrderBy(ctx context.Context, sortField string) ([]models.User, error) {
    // 白名单验证
    allowedFields := map[string]bool{
        "id":         true,
        "name":       true,
        "created_at": true,
    }

    if !allowedFields[sortField] {
        return nil, errors.New("invalid sort field")
    }

    var users []models.User
    // 验证通过后,可以安全地拼接列名
    err := s.db.WithContext(ctx).
        Order(sortField + " DESC").
        Find(&users).Error

    return users, err
}

// ✅ 正确 - 白名单验证表名
func (s *AdminService) QueryTable(ctx context.Context, tableName string) (int64, error) {
    allowedTables := map[string]bool{
        "users":    true,
        "products": true,
        "orders":   true,
    }

    if !allowedTables[tableName] {
        return 0, errors.New("invalid table name")
    }

    var count int64
    err := s.db.WithContext(ctx).Table(tableName).Count(&count).Error
    return count, err
}

安全最佳实践总结

  • 必须使用 Where() 占位符 ? 或命名占位符
  • 必须使用结构体条件或 map 条件
  • 必须对动态列名/表名进行白名单验证
  • 禁止使用 fmt.Sprintf() 拼接 SQL 字符串
  • 禁止使用字符串拼接构造 WHERE 条件
  • 禁止直接使用未验证的用户输入作为列名或表名

禁止的做法

  • 禁止 使用 index、uniqueIndex、primaryKey 标签
  • 禁止 使用 AutoMigrate
  • 禁止 定义外键关系(foreignKey、references)
  • 禁止 使用 Preload/Joins 自动加载(性能问题)
go
// ❌ 错误 - 禁止使用索引标签
type User struct {
    ID    int64  `gorm:"primaryKey"`      // 禁止
    Email string `gorm:"uniqueIndex"`     // 禁止
}

// ❌ 错误 - 禁止 AutoMigrate
db.AutoMigrate(&User{})  // 禁止

// ❌ 错误 - 禁止定义关联
type Order struct {
    UserID int64 `gorm:"foreignKey:UserID"`  // 禁止
}

特殊场景

软删除查询

go
// ✅ 正确 - 默认过滤软删除
db.Where("deleted_at IS NULL").Find(&users)

// ✅ 正确 - 包含软删除记录
db.Find(&users)

Context 和超时

go
// ✅ 正确 - 超时控制
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

err := s.db.WithContext(ctx).Find(&users).Error

相关文档