跳到主要内容

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/unixtimegorm:"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--
更新(单字段)102ms210ms/1000条381%
删除98ms185ms/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 最佳实践

  1. 为所有模型实现TableName()方法显式指定表名
  2. 复杂查询使用链式调用构建,提高可读性
  3. 批量操作时控制批次大小,避免内存溢出
  4. 敏感字段更新使用Select()明确指定字段,防止安全问题
  5. 实现模型验证逻辑,在钩子函数中进行数据校验

2.7.3 扩展阅读

作者:GO兔 博客:https://luckxgo.cn 分享大家都看得懂的博客 关注公众号:GO兔开源