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 类型 | 说明 |
|---|---|---|
| BIGINT | int64 | 主键、外键、时间戳 |
| INT | int / int32 | 普通整数 |
| VARCHAR | string | 字符串 |
| TEXT | string | 长文本 |
| JSON | string | JSON 数据 |
| DECIMAL | float64 | 精确小数 |
| 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