GORM入门:ORM映射与CRUD
2026/8/16 15:45:49 网站建设 项目流程

GORM入门:ORM映射与CRUD

摘要: 本篇从GORM的安装和连接讲起,演示模型定义与表结构映射、AutoMigrate自动建表、完整的CRUD操作和条件查询技巧,讲解软删除机制和Unscoped用法,分享软删除导致唯一索引冲突的踩坑经历,对比database/sql、sqlx和GORM三种数据访问方式的开发效率。

开篇故事

上一篇我们用database/sql写了一套CRUD,每个查询都要手动Scan字段,加一个字段就得改SQL和Scan两处。我有个项目用户表改了三次结构,每次改完都要全局搜索所有涉及User的SQL和Scan,生怕漏一个。

后来我把这个项目迁移到GORM。定义好struct,GORM自动处理字段映射,加字段只需要改struct和调一次AutoMigrate。开发效率提升非常明显,原来一个接口要写30分钟SQL和绑定代码,迁移后10分钟搞定。

当然GORM也有它的坑。这篇我们从入门开始,把CRUD和模型映射讲透,坑留到后面说。

一、安装与连接

packagemainimport("log""time""gorm.io/driver/mysql""gorm.io/gorm")funcmain(){// DSN与database/sql的格式一致dsn:="root:123456@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=true&loc=Local"// gorm.Open建立连接并初始化// 第二个参数是配置项,后面性能篇会详细讲db,err:=gorm.Open(mysql.Open(dsn),&gorm.Config{})iferr!=nil{log.Fatal("连接数据库失败:",err)}// 获取底层sql.DB配置连接池// GORM底层仍然使用database/sqlsqlDB,_:=db.DB()sqlDB.SetMaxOpenConns(25)// 最大连接数sqlDB.SetMaxIdleConns(10)// 最大空闲连接数sqlDB.SetConnMaxLifetime(5*time.Minute)// 连接最大存活时间log.Println("GORM连接成功")}

GORM底层仍然使用database/sql,所以连接池配置方式完全一样。gorm.Open返回的是*gorm.DB,通过db.DB()可以拿到底层的*sql.DB。

二、模型定义与自动建表

GORM用struct tag来映射数据库表结构。

import("time""gorm.io/gorm")// User 用户模型// GORM约定struct名为User,表名为userstypeUserstruct{// ID是GORM的默认主键// 类型为uint,对应数据库BIGINT UNSIGNEDIDuint`gorm:"primaryKey"`// size控制VARCHAR长度,not null非空约束Namestring`gorm:"size:50;not null"`// uniqueIndex创建唯一索引Emailstring`gorm:"size:100;uniqueIndex"`// default设置默认值Ageint`gorm:"default:18"`Statusstring`gorm:"size:20;default:active"`// CreatedAt和UpdatedAt自动管理CreatedAt time.Time UpdatedAt time.Time// DeletedAt软删除字段// 有值表示已删除,查询时自动过滤DeletedAt gorm.DeletedAt`gorm:"index"`}

AutoMigrate会根据struct创建或更新表结构。

// AutoMigrate自动建表// 已存在的表会追加缺失的列和索引// 不会删除已有的列,也不会修改列类型db.AutoMigrate(&User{})// 生成的SQL大概是这样// CREATE TABLE users (// id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,// name VARCHAR(50) NOT NULL,// email VARCHAR(100),// age BIGINT DEFAULT 18,// ...// PRIMARY KEY (id)// )

注意AutoMigrate是追加式的,只加不删。如果你删掉struct里的某个字段,数据库表里对应的列还在。生产环境不建议用AutoMigrate管理表结构,应该用migration工具比如golang-migrate做版本化迁移。

三、CRUD操作

// Create 插入记录funcCreate(db*gorm.DB){user:=User{Name:"张三",Email:"zhangsan@example.com",Age:25,}// Create插入一条记录result:=db.Create(&user)// result.Error 错误信息// result.RowsAffected 受影响行数// user.ID 自动回填插入后的自增IDfmt.Println("新用户ID:",user.ID)}// 批量插入funcBatchCreate(db*gorm.DB){users:=[]User{{Name:"李四",Email:"lisi@example.com",Age:30},{Name:"王五",Email:"wangwu@example.com",Age:28},}// 传入切片批量插入db.Create(&users)// 每个元素的ID都会自动回填}// Read 查询操作funcRead(db*gorm.DB){varuser User// First按主键查询,没找到返回ErrRecordNotFounddb.First(&user,1)// SQL: SELECT * FROM users WHERE id = 1 LIMIT 1// 按条件查询db.First(&user,"email = ?","zhangsan@example.com")// 查询多条varusers[]User db.Find(&users,"age > ?",20)// SQL: SELECT * FROM users WHERE age > 20// 链式条件查询db.Where("name LIKE ?","%张%").Where("age >= ?",20).Order("age DESC").Limit(10).Offset(0).Find(&users)// SQL: SELECT * FROM users// WHERE name LIKE '%张%' AND age >= 20// ORDER BY age DESC LIMIT 10 OFFSET 0}
// Update 更新操作funcUpdate(db*gorm.DB){varuser User db.First(&user,1)// Save保存所有字段user.Age=26db.Save(&user)// SQL: UPDATE users SET ... WHERE id = 1// Update更新单个字段db.Model(&user).Update("name","张三丰")// SQL: UPDATE users SET name = '张三丰' WHERE id = 1// Updates更新多个字段// 用map可以更新零值db.Model(&user).Updates(map[string]interface{}{"name":"张三","age":27,})// SQL: UPDATE users SET name = '张三', age = 27 WHERE id = 1}// Delete 删除操作funcDelete(db*gorm.DB){// 软删除,设置deleted_at字段的值// 查询时GORM自动过滤已软删除的记录db.Delete(&User{},1)// SQL: UPDATE users SET deleted_at = NOW() WHERE id = 1// Unscoped查询包含软删除的记录varusers[]User db.Unscoped().Find(&users)// SQL: SELECT * FROM users (不带deleted_at过滤)// Unscoped永久删除db.Unscoped().Delete(&User{},1)// SQL: DELETE FROM users WHERE id = 1}

四、条件查询技巧

// 常用查询方式汇总funcQueryExamples(db*gorm.DB){varusers[]User// IN查询db.Where("id IN ?",[]int{1,2,3}).Find(&users)// BETWEEN范围查询db.Where("age BETWEEN ? AND ?",20,30).Find(&users)// OR条件db.Where("name = ?","张三").Or("name = ?","李四").Find(&users)// Select指定字段,减少网络传输量db.Select("id","name").Find(&users)// SQL: SELECT id, name FROM users// Count计数varcountint64db.Model(&User{}).Where("age > ?",20).Count(&count)// 分组与聚合查询typeResultstruct{AgeintTotalint}varresults[]Result db.Model(&User{}).Select("age, count(*) as total").Group("age").Find(&results)}

五、独家踩坑:软删除的意外行为

GORM默认开启软删除,这给了我一个很大的"惊喜"。

// 踩坑场景:唯一索引与软删除冲突typeUserstruct{IDuintEmailstring`gorm:"uniqueIndex"`// 唯一索引NamestringDeletedAt gorm.DeletedAt`gorm:"index"`// 软删除}// 用户注销后重新注册funcReRegister(db*gorm.DB,emailstring)error{// 先删除旧账号(软删除)db.Where("email = ?",email).Delete(&User{})// deleted_at被设值,但记录还在表里// 再创建新账号user:=User{Email:email,Name:"新用户"}err:=db.Create(&user).Error// 报错: Duplicate entry 'xxx' for key 'idx_email'// 唯一索引不关心deleted_at,旧记录还在returnerr}

用户注销后想用同一个邮箱重新注册,结果唯一索引报重复。软删除只是把deleted_at设了值,记录还在表里,唯一索引照样拦截。

这个问题有好几种解法。可以把唯一索引改成联合索引(email, deleted_at),但deleted_at是NULL时联合索引的行为在不同数据库有差异。也可以在业务层处理,注销时把email加个后缀比如deleted_123@xxx.com。最干净的方式是注销时判断是否已有软删除记录,有就恢复更新而不是新建。

软删除在大多数场景很方便,但涉及唯一约束时要特别小心。

六、对比分析

特性database/sqlsqlxGORM
开发效率
SQL控制力完全手动完全手动自动生成
结构体映射手动Scan自动Scan自动映射
软删除内置
迁移工具AutoMigrate
学习成本

从database/sql到GORM,开发效率提升很大,代价是失去了对SQL的完全控制。简单CRUD用GORM很爽,复杂查询GORM的链式API有时候比直接写SQL还难读。下篇我们讲GORM进阶,关联关系和事务,那里GORM的优势和劣势都会更明显。

总结

GORM把重复的数据库操作代码抽象掉了,模型定义即表结构,CRUD一行搞定。记住两个要点,struct tag控制映射规则,软删除影响唯一索引。下一篇进入GORM进阶,讲关联关系、钩子和事务。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询