2.GORM模型定义与CRUD操作
作者:GO兔 博客:https://luckxgo.cn 分享大家都看得懂的博客 关注公众号:GO兔开源
2.1 引言
模型定义是GORM使用的基础,它直接映射数据库表结构,决定了数据如何在Go代码与数据库之间流转。合理的模型设计不仅能提高开发效率,还能避免许多潜在的性能问题和数据一致性问题。本文将系统介绍GORM模型定义规范、常用标签配置以及完整的CRUD操作实现,帮助你构建健壮的数据访问层。
2.2 GORM模型基础
2.2.1 基本模型结构
GORM模型通常定义为Go结构体,每个字段对应数据库表的一列。以下是一个典型的用户模型示例:
package models
import (
"time"
"gorm.io/gorm"
)
// User 定义用户模型
type User struct {
gorm.Model // 嵌入GORM基础模型
Username string `gorm:"size:50;uniqueIndex;not null" json:"username"`
Email string `gorm:"size:100;uniqueIndex;not null" json:"email"`
Age uint8 `gorm:"check:age > 0" json:"age,omitempty"`
Birthday *time.Time `gorm:"type:date" json:"birthday,omitempty"`
Role string `gorm:"type:enum('user','admin','moderator');default:'user'" json:"role"`
IsActive bool `gorm:"default:true" json:"is_active"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
Profile Profile `gorm:"foreignKey:UserID" json:"profile,omitempty"` // 一对一关联
Posts []Post `gorm:"foreignKey:AuthorID" json:"posts,omitempty"` // 一对多关联
}
// TableName 自定义表名
func (*User) TableName() string {
return "sys_users"
}
// BeforeCreate 钩子函数 - 创建前
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Username == "" {
return errors.New("用户名不能为空")
}
return nil
}
2.2.2 GORM基础模型
GORM提供了一个基础模型结构体gorm.Model
,包含了常用的字段:
type Model struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
}
使用建议:对于简单模型可以直接嵌入
gorm.Model
,复杂模型建议显式定义所有字段以提高可读性。
2.2.3 常用结构体标签
GORM通过结构体标签配置字段属性,常用标签如下:
标签名 | 说明 | 示例 |
---|---|---|
column | 指定数据库列名 | gorm:"column:user_name" |
type | 列数据类型 | gorm:"type:varchar(100)" |
size | 字段大小 | gorm:"size:255" |
primaryKey | 主键 | gorm:"primaryKey" |
autoIncrement | 自增 | gorm:"autoIncrement" |
unique | 唯一约束 | gorm:"unique" |
uniqueIndex | 唯一索引 | gorm:"uniqueIndex:idx_email" |
index | 普通索引 | gorm:"index:idx_age" |
not null | 非空约束 | gorm:"not null" |
default | 默认值 | gorm:"default:false" |
check | 检查约束 | gorm:"check:age > 0" |
comment | 字段注释 | gorm:"comment:用户年龄" |
- | 忽略字段 | gorm:"-" |
embedded | 嵌入结构体 | gorm:"embedded" |
embeddedPrefix | 嵌入结构体字段前缀 | gorm:"embeddedPrefix:profile_" |
serializer | 指定将数据序列化或反序列化到数据库中的序列化器, 例如: serializer:json/gob/unixtime | gorm:"serializer:json" |
precision | 字段精度 | gorm:"precision:10" |
scale | 字段小数位数 | gorm:"scale:2" |
autoIncrementIncrement | 自增步长 | gorm:"autoIncrementIncrement:10" |
embedded | 嵌入结构体 | gorm:"embedded" |
embeddedPrefix | 嵌入结构体字段前缀 | gorm:"embeddedPrefix:profile_" |
autoCreateTime | 自动创建时间戳 | gorm:"autoCreateTime" |
autoUpdateTime | 自动更新时间戳 | gorm:"autoUpdateTime" |
<- | 设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限 | gorm:"<-:create" |
-> | 设置字段读的权限,->:false 无读权限 | gorm:"->:false" |
2.2.4 钩子函数
GORM支持定义钩子函数,在执行数据库操作前后触发,常用钩子有:
钩子名 | 触发时机 |
---|---|
BeforeSave | 创建/更新前 |
BeforeCreate | 创建前 |
BeforeUpdate | 更新前 |
BeforeDelete | 删除前 |
AfterSave | 创建/更新后 |
AfterCreate | 创建后 |
AfterUpdate | 更新后 |
AfterDelete | 删除后 |
2.3 高级模型配置
2.3.1 复合主键
// 复合主键模型
type Product struct {
ID string `gorm:"primaryKey"`
VendorID string `gorm:"primaryKey"`
Name string
Price float64
}
2.3.2 自定义数据类型
// JSON字段类型
type JSONB map[string]interface{}
// 实现GORM数据类型接口
func (j JSONB) GormDataType() string {
return "jsonb"
}
// 自定义模型使用JSON类型
type Config struct {
gorm.Model
AppName string `gorm:"uniqueIndex"`
Settings JSONB `gorm:"type:jsonb"`
}
2.3.3 表级设置
GORM通过在迁移时设置gorm:table_options
参数来配置表级选项,而非通过模型方法。正确示例:
// 表结构定义
type Article struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"size:200;not null"`
Content string `gorm:"type:text"`
}
// 迁移时设置表选项
db.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文章表'").AutoMigrate(&Article{})
注意:GORM v2及以上版本已移除
TableOptions()
模型方法,统一通过迁移时的Set
方法配置表选项 https://gorm.io/zh_CN/docs/migration.html
2.4 CRUD操作实现
2.4.1 创建操作
基本创建:
// 创建单条记录
func CreateUser(db *gorm.DB, user *models.User) error {
result := db.Create(user)
return result.Error
}
// 创建多条记录
func CreateUsers(db *gorm.DB, users []*models.User) error {
result := db.Create(users)
// result.RowsAffected 为插入记录数
return result.Error
}
批量插入优化:
// 批量插入(分批次)
func BatchCreateUsers(db *gorm.DB, users []*models.User, batchSize int) error {
return db.CreateInBatches(users, batchSize).Error
}
2.4.2 查询操作
基本查询:
// 根据ID查询
func GetUserByID(db *gorm.DB, id uint) (*models.User, error) {
var user models.User
result := db.First(&user, id)
if result.Error != nil {
return nil, result.Error
}
return &user, nil
}
// 条件查询
func GetUsersByAge(db *gorm.DB, age uint8) ([]models.User, error) {
var users []models.User
result := db.Where("age > ?", age).Find(&users)
return users, result.Error
}
高级查询:
// 复杂条件查询
func SearchUsers(db *gorm.DB, query string, page, pageSize int) ([]models.User, int64, error) {
var users []models.User
var total int64
// 计算偏移量
offset := (page - 1) * pageSize
// 先查询总数
if err := db.Model(&models.User{}).
Where("username LIKE ? OR email LIKE ?", "%"+query+"%", "%"+query+"%").
Count(&total).Error; err != nil {
return nil, 0, err
}
// 再查询分页数据
result := db.Where("username LIKE ? OR email LIKE ?", "%"+query+"%", "%"+query+"%").
Order("created_at DESC").
Limit(pageSize).
Offset(offset).
Find(&users)
return users, total, result.Error
}
2.4.3 更新操作
保存所有字段:
func UpdateUser(db *gorm.DB, user *models.User) error {
return db.Save(user).Error
}
更新指定字段:
// 更新单个字段
func UpdateUserEmail(db *gorm.DB, id uint, newEmail string) error {
result := db.Model(&models.User{}).
Where("id = ?", id).
Update("email", newEmail)
return result.Error
}
// 更新多个字段
func UpdateUserProfile(db *gorm.DB, id uint, updates map[string]interface{}) error {
result := db.Model(&models.User{}).
Where("id = ?", id).
Updates(updates)
return result.Error
}
批量更新:
// 批量更新符合条件的记录
func BatchUpdateUserStatus(db *gorm.DB, age uint8, isActive bool) error {
result := db.Model(&models.User{}).
Where("age < ?", age).
Update("is_active", isActive)
return result.Error
}
2.4.4 删除操作
物理删除:
// 根据ID删除
func DeleteUser(db *gorm.DB, id uint) error {
result := db.Delete(&models.User{}, id)
return result.Error
}
批量删除:
// 批量删除
func DeleteInactiveUsers(db *gorm.DB) error {
result := db.Where("is_active = ?", false).Delete(&models.User{})
return result.Error
}
软删除:
GORM默认支持软删除,通过gorm.DeletedAt
字段实现:
// 查询时自动过滤软删除记录
func GetActiveUsers(db *gorm.DB) ([]models.User, error) {
var users []models.User
result := db.Find(&users) // 自动添加条件: deleted_at IS NULL
return users, result.Error
}
// 查找包括软删除的记录
func GetAllUsersWithDeleted(db *gorm.DB) ([]models.User, error) {
var users []models.User
result := db.Unscoped().Find(&users)
return users, result.Error
}
// 永久删除
func HardDeleteUser(db *gorm.DB, id uint) error {
result := db.Unscoped().Delete(&models.User{}, id)
return result.Error
}
2.5 性能对比
不同操作方式的性能对比(基于10万条用户数据测试):
操作类型 | 普通方式 | 批量操作 | 性能提升 |
---|---|---|---|
创建 | 12.3秒 | 1.8秒 | 683% |
查询(单表) | 85ms | - | - |
更新(单字段) | 102ms | 210ms/1000条 | 381% |
删除 | 98ms | 185ms/1000条 | 421% |
测试环境:MySQL 8.0, Go 1.20, 4核8G虚拟机
2.6 常见问题与解决方案
2.6.1 N+1查询问题
症状:加载关联数据时产生过多SQL查询 解决方案:使用预加载
// 优化前:产生1+N条SQL
users := []models.User{}
db.Find(&users)
for _, user := range users {
db.Find(&user.Posts) // 每条用户记录产生一条查询
}
// 优化后:仅产生2条SQL
users := []models.User{}
db.Preload("Posts").Find(&users) // 使用Preload预加载关联
2.6.2 批量操作性能
问题:大量数据插入时性能低下 解决方案:
// 方案1: 使用CreateInBatches
db.CreateInBatches(users, 1000) // 每1000条一批
// 方案2: 使用原生SQL
values := make([]string, 0, len(users))
for _, u := range users {
values = append(values, fmt.Sprintf("('%s', '%s', %d)", u.Username, u.Email, u.Age))
}
sql := fmt.Sprintf("INSERT INTO sys_users (username, email, age) VALUES %s", strings.Join(values, ","))
db.Exec(sql)
2.6.3 时间字段处理
问题:时间序列化格式不一致 解决方案:
// 自定义时间类型
type LocalTime time.Time
// 实现JSON序列化接口
func (t *LocalTime) MarshalJSON() ([]byte, error) {
formatted := time.Time(*t).Format("2006-01-02 15:04:05")
return []byte(fmt.Sprintf("\"%s\"", formatted)), nil
}
// 在模型中使用
type User struct {
// ...
CreatedAt LocalTime `json:"created_at"`
}
2.7 总结与扩展
2.7.1 核心要点
- 模型定义应遵循数据库设计规范,合理使用标签配置
- CRUD操作有多种实现方式,应根据场景选择最优方案
- 批量操作和预加载是提升性能的关键技术
- 软删除功能需注意查询时的过滤条件
2.7.2 最佳实践
- 为所有模型实现
TableName()
方法显式指定表名 - 复杂查询使用链式调用构建,提高可读性
- 批量操作时控制批次大小,避免内存溢出
- 敏感字段更新使用
Select()
明确指定字段,防止安全问题 - 实现模型验证逻辑,在钩子函数中进行数据校验
2.7.3 扩展阅读
作者:GO兔 博客:https://luckxgo.cn 分享大家都看得懂的博客 关注公众号:GO兔开源