Go Gorm框架使用详解:从入门到实战(含常见问题)
做Go后端开发的同学都知道,直接写SQL容易出现冗余代码,还得处理数据映射和sql 安全性问题。
Gorm作为Go生态里最流行的ORM框架,能完美解决这些痛点。
我用Gorm开发过多个项目,从简单的单表操作到复杂的关联查询都踩过坑,
今天整理了使用方法和避坑经验分享出来,新手也能快速上手。帮助大家快速开发
直接 gorm 的地址
gorm.io/gorm官方文档:https://gorm.io/zh_CN/docs/index.html
一、数据库连接
Gorm连接数据库需要配置DSN(数据源名称),然后通过gorm.Open方法初始化数据库连接。创建main.go文件,写入以下代码:
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"log"
)
func main() {
// 配置MySQL DSN:用户名:密码@tcp(主机:端口)/数据库名?charset=utf8mb4&parseTime=True&loc=Local
// 请根据实际情况替换用户名、密码、主机和端口
dsn := "root:123456@tcp(127.0.0.1:3306)/gorm_demo?charset=utf8mb4&parseTime=True&loc=Local"
// 连接数据库并获取DB实例
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("数据库连接失败:%v", err)
}
log.Println("数据库连接成功")
// 可选:获取数据库底层sql.DB对象,配置连接池
sqlDB, err := db.DB()
if err != nil {
log.Fatalf("获取sql.DB对象失败:%v", err)
}
// 设置最大空闲连接数
sqlDB.SetMaxIdleConns(10)
// 设置最大打开连接数
sqlDB.SetMaxOpenConns(100)
// 设置连接最大存活时间
sqlDB.SetConnMaxLifetime(300 * time.Second)
}这里有几个关键注意点:DSN中的parseTime=True是MySQL驱动必需的配置,否则会出现时间解析错误;通过db.DB()获取的sql.DB对象可以配置连接池参数,合理的连接池配置能提升系统并发性能。
二、模型定义规范
Gorm采用结构体作为模型映射数据库表,通过结构体字段标签(tag)配置表名、字段名、数据类型等信息。
以用户表(user)为例,定义模型如下:
import "time"
// User 用户模型,映射user表
type User struct {
// gorm:"primaryKey" 标识主键,默认会自增
ID uint `gorm:"primaryKey"`
// gorm:"column:username;type:varchar(50);not null;unique" 配置字段名、类型、非空、唯一
Username string `gorm:"column:username;type:varchar(50);not null;unique"`
// gorm:"column:password;type:varchar(100);not null" 密码字段建议存储加密后的字符串
Password string `gorm:"column:password;type:varchar(100);not null"`
// gorm:"column:age;type:int;default:0" 配置默认值
Age int `gorm:"column:age;type:int;default:0"`
// gorm:"column:email" 字段名与结构体字段名一致时,column标签可省略
Email string `gorm:"column:email"`
// gorm:"column:created_at;autoCreateTime" 自动填充创建时间
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
// gorm:"column:updated_at;autoUpdateTime" 自动填充更新时间
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
// gorm:"column:deleted_at;default:null" 软删除字段,需要配合gorm.DeleteAt使用
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;default:null" json:"-"`
}
// TableName 自定义表名,如果不实现该方法,默认表名为结构体名的复数形式(users)
func (u User) TableName() string {
return "user"
}模型定义的核心标签说明:primaryKey指定主键,支持复合主键;column映射数据库字段名,解决结构体字段名与数据库字段名不一致问题;autoCreateTime和autoUpdateTime实现时间自动填充;DeletedAt字段实现软删除功能,删除时不会真正删除数据,只是更新该字段值。
定义好模型后,通过以下代码自动创建数据表:
// 自动迁移创建表,会根据模型定义创建或更新表结构
err = db.AutoMigrate(&User{})
if err != nil {
log.Fatalf("创建表失败:%v", err)
}三、创建数据(Create)
Gorm提供了单条数据创建和批量创建两种方式,满足不同场景需求:
// 1. 单条数据创建
func createUser(db *gorm.DB) {
user := User{
Username: "zhangsan",
Password: "123456", // 实际开发中务必加密存储,这里仅做演示
Age: 25,
Email: "zhangsan@example.com",
}
// Create方法创建数据,会自动填充CreatedAt和UpdatedAt字段
result := db.Create(&user)
if result.Error != nil {
log.Printf("创建用户失败:%v", result.Error)
return
}
log.Printf("创建成功,用户ID:%d", user.ID) // 创建成功后,主键ID会自动赋值给user对象
}
// 2. 批量创建
func batchCreateUser(db *gorm.DB) {
users := []User{
{Username: "lisi", Password: "654321", Age: 23, Email: "lisi@example.com"},
{Username: "wangwu", Password: "112233", Age: 28, Email: "wangwu@example.com"},
}
// CreateInBatches方法批量创建,第二个参数指定每次批量插入的数量
result := db.CreateInBatches(users, 2)
if result.Error != nil {
log.Printf("批量创建用户失败:%v", result.Error)
return
}
log.Printf("批量创建成功,影响行数:%d", result.RowsAffected)
}这里需要注意的是,批量创建时指定的批量大小要根据数据库性能调整,过大可能导致插入失败,过小则影响效率。
实际开发中密码一定要用bcrypt等算法加密后再存储,绝对不能明文存储。
四、查询数据(Read)
查询是最复杂的操作,Gorm提供了丰富的查询方法,支持单条查询、多条查询、条件查询、关联查询等。
以下是常用查询场景实现:
// 1. 根据主键查询单条数据
func getUserByID(db *gorm.DB, id uint) {
var user User
// First方法根据主键查询第一条数据,若查询不到返回ErrRecordNotFound错误
result := db.First(&user, id)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
log.Printf("未找到ID为%d的用户", id)
return
}
log.Printf("查询用户失败:%v", result.Error)
return
}
log.Printf("查询到用户:%+v", user)
}
// 2. 条件查询多条数据
func getUsersByCondition(db *gorm.DB) {
var users []User
// Where方法指定查询条件,支持多种条件格式
// 方式1:字符串条件(注意避免SQL注入,复杂条件建议用参数化)
// db.Where("age > ? and email like ?", 25, "%example.com%").Find(&users)
// 方式2:结构体条件(只匹配非零值字段)
// db.Where(&User{Age: 25, Email: "zhangsan@example.com"}).Find(&users)
// 方式3:map条件
result := db.Where(map[string]interface{}{
"age": 25,
"email": "%example.com%",
}).Find(&users)
if result.Error != nil {
log.Printf("条件查询用户失败:%v", result.Error)
return
}
log.Printf("查询到%d个用户:%+v", len(users), users)
}
// 3. 排序、分页查询
func getUsersWithSortAndPage(db *gorm.DB) {
var users []User
page := 1 // 当前页码
pageSize := 2 // 每页条数
offset := (page - 1) * pageSize // 计算偏移量
// Order方法指定排序字段(desc降序,asc升序),Limit指定每页条数,Offset指定偏移量
result := db.Order("age desc").Limit(pageSize).Offset(offset).Find(&users)
if result.Error != nil {
log.Printf("分页查询用户失败:%v", result.Error)
return
}
log.Printf("第%d页用户数据:%+v", page, users)
// 统计总条数
var total int64
db.Model(&User{}).Count(&total)
log.Printf("用户总条数:%d", total)
}查询时的注意事项:First方法查询不到数据会返回错误,需单独处理;
条件查询用?做参数占位符可防止SQL注入;分页查询时务必计算好偏移量,避免出现重复或遗漏数据。
五、更新数据(Update)
Gorm支持全量更新和部分更新,实际开发中部分更新使用更广泛,避免覆盖未修改的字段:
// 1. 部分更新(推荐)
func updateUser(db *gorm.DB, id uint) {
// 方式1:使用Save方法,需要先查询出数据再修改(会更新所有字段)
// var user User
// db.First(&user, id)
// user.Age = 26
// db.Save(&user)
// 方式2:使用Update/Updates方法,直接指定更新字段(推荐)
// Update更新单个字段
// result := db.Model(&User{}).Where("id = ?", id).Update("age", 26)
// Updates更新多个字段,支持结构体或map
result := db.Model(&User{}).Where("id = ?", id).Updates(map[string]interface{}{
"age": 26,
"email": "zhangsan_update@example.com",
})
if result.Error != nil {
log.Printf("更新用户失败:%v", result.Error)
return
}
log.Printf("更新成功,影响行数:%d", result.RowsAffected)
}
// 2. 批量更新
func batchUpdateUser(db *gorm.DB) {
// 更新所有年龄大于25的用户,将年龄加1
result := db.Model(&User{}).Where("age > ?", 25).Update("age", gorm.Expr("age + ?", 1))
if result.Error != nil {
log.Printf("批量更新用户失败:%v", result.Error)
return
}
log.Printf("批量更新成功,影响行数:%d", result.RowsAffected)
}更新时的关键技巧:使用Model方法指定模型,Where方法指定更新条件,避免出现无条件更新导致全表数据被修改;gorm.Expr用于实现复杂的更新逻辑,比如字段自增、自减
六、删除数据(Delete)
Gorm支持物理删除和软删除,软删除不会真正删除数据,只是标记删除状态,便于数据恢复:
// 1. 软删除(模型中定义了DeletedAt字段时默认是软删除)
func softDeleteUser(db *gorm.DB, id uint) {
result := db.Delete(&User{}, id)
if result.Error != nil {
log.Printf("软删除用户失败:%v", result.Error)
return
}
log.Printf("软删除成功,影响行数:%d", result.RowsAffected)
// 软删除后查询不到该数据
var user User
if db.First(&user, id).Error != nil {
log.Printf("软删除后查询不到用户:%v", result.Error)
}
// 查询包含软删除的数据
var deletedUser User
db.Unscoped().First(&deletedUser, id)
log.Printf("查询到软删除的用户:%+v", deletedUser)
}
// 2. 物理删除(彻底删除数据,谨慎使用)
func hardDeleteUser(db *gorm.DB, id uint) {
// 使用Unscoped方法实现物理删除
result := db.Unscoped().Delete(&User{}, id)
if result.Error != nil {
log.Printf("物理删除用户失败:%v", result.Error)
return
}
log.Printf("物理删除成功,影响行数:%d", result.RowsAffected)
}删除操作的重要提醒:软删除是Gorm的默认行为,若要物理删除必须加上Unscoped方法;物理删除后数据无法恢复,生产环境中除非有明确需求,否则优先使用软删除。
七、关联查询
以“用户-订单”一对多关系为例,演示关联查询的实现。首先定义订单模型:
// Order 订单模型,与User是一对多关系(一个用户多个订单)
type Order struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"column:user_id;not null"` // 外键,关联User表的ID
OrderNo string `gorm:"column:order_no;type:varchar(50);not null;unique"`
Amount float64 `gorm:"column:amount;type:decimal(10,2);not null"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;default:null" json:"-"`
// 关联User模型,BelongsTo表示Order属于User
User User `gorm:"foreignKey:UserID"`
}
// 同时在User模型中添加关联字段
type User struct {
// 原有字段省略...
Orders []Order `gorm:"foreignKey:UserID"` // HasMany表示User拥有多个Order
}关联查询示例:
// 1. 查询用户及其关联的所有订单(预加载)
func getUserWithOrders(db *gorm.DB, userID uint) {
var user User
// Preload方法预加载关联的Orders数据,避免N+1查询问题
result := db.Preload("Orders").First(&user, userID)
if result.Error != nil {
log.Printf("查询用户及订单失败:%v", result.Error)
return
}
log.Printf("用户:%s,订单数量:%d,订单列表:%+v", user.Username, len(user.Orders), user.Orders)
}
// 2. 通过订单查询关联的用户
func getOrderWithUser(db *gorm.DB, orderID uint) {
var order Order
// Joins方法关联查询用户数据
result := db.Joins("User").First(&order, orderID)
if result.Error != nil {
log.Printf("查询订单及用户失败:%v", result.Error)
return
}
log.Printf("订单号:%s,所属用户:%s", order.OrderNo, order.User.Username)
}关联查询的关键技巧:Preload方法用于预加载关联数据,解决N+1查询性能问题;Joins方法用于关联查询,适用于需要关联过滤的场景;定义关联时务必指定正确的外键(foreignKey)。
八、事务处理
当需要执行多个关联的数据库操作时,事务能保证这些操作要么全部成功,要么全部失败,避免数据不一致。Gorm事务使用Begin、Commit、Rollback方法实现:
// 事务示例:创建用户并创建关联订单
func createUserWithOrder(db *gorm.DB) {
// 1. 开启事务
tx := db.Begin()
if tx.Error != nil {
log.Printf("开启事务失败:%v", tx.Error)
return
}
// 2. 延迟执行回滚,若事务提交成功则不会执行
defer func() {
if r := recover(); r != nil {
tx.Rollback()
log.Printf("事务执行异常,已回滚:%v", r)
}
}()
// 3. 执行事务操作1:创建用户
user := User{Username: "zhaoliu", Password: "666666", Age: 30, Email: "zhaoliu@example.com"}
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
log.Printf("创建用户失败,事务回滚:%v", err)
return
}
// 4. 执行事务操作2:创建关联订单
order := Order{UserID: user.ID, OrderNo: "20240501001", Amount: 199.99}
if err := tx.Create(&order).Error; err != nil {
tx.Rollback()
log.Printf("创建订单失败,事务回滚:%v", err)
return
}
// 5. 提交事务
if err := tx.Commit().Error; err != nil {
log.Printf("事务提交失败:%v", err)
return
}
log.Println("事务执行成功:创建用户及订单成功")
}事务处理的注意事项:开启事务后,所有操作都要使用tx实例执行,不能再用原来的db实例;必须添加错误处理和回滚逻辑,避免出现事务开启后未提交也未回滚的情况;使用recover捕获恐慌,确保异常时能正常回滚。
常见问题
Q1、模型字段与数据库字段不匹配
问题现象:查询或更新时出现“unknown column”错误,或字段值无法正确映射。
常见原因:
- 结构体字段未添加
column标签,且字段名与数据库字段名大小写或命名风格不一致(如结构体字段为Username,数据库字段为user_name); - 字段类型不匹配,如结构体字段为int,数据库字段为varchar。
解决方案:
- 为结构体字段添加
column标签,明确映射数据库字段名,如Username stringgorm:“column:user_name”``; - 统一结构体字段类型与数据库字段类型,必要时使用类型转换;
- 开启Gorm的命名策略配置,实现自动映射,如:
import "gorm.io/gorm/schema"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true, // 表名不加复数后缀
NoLowerCase: false, // 自动将驼峰命名转为蛇形命名
},
})Q2、软删除后无法查询到数据
问题现象:执行删除操作后,用普通查询方法无法查询到数据,但数据库中数据仍存在。
常见原因:模型中定义了gorm.DeletedAt字段,Gorm默认执行软删除,查询时会自动添加deleted_at IS NULL条件。
解决方案:
- 若要查询包含软删除的数据,使用
Unscoped方法,如db.Unscoped().Find(&users); - 若不需要软删除功能,删除模型中的
DeletedAt字段即可。
Q3、事务提交后数据未生效
问题现象:事务中执行了创建或更新操作,提交事务后数据库中无对应数据。
常见原因:
- 事务操作时使用了原来的
db实例,而非事务开启后的tx实例; - 事务执行过程中出现错误,但未正确捕获,导致事务未提交;
- 数据库引擎不支持事务,如MySQL的MyISAM引擎。
解决方案:
- 事务中的所有数据库操作必须使用
tx实例执行; - 完善事务中的错误处理,确保每个操作的错误都能被捕获并执行回滚;
- 将数据库表引擎改为支持事务的引擎,如InnoDB。
Q4、关联查询出现N+1问题
问题现象:查询列表数据时,先执行1条查询主表数据的SQL,再执行N条查询关联表数据的SQL,性能低下。
常见原因:未使用预加载功能,而是在循环中查询关联数据。
解决方案:
- 使用
Preload方法预加载关联数据,如db.Preload("Orders").Find(&users); - 对于复杂的关联查询,使用
Joins方法进行关联查询,减少SQL执行次数。
Q5、预加载如何实现排序?
请移步文章:gorm实现多对多映射以及预加载排序
Q6、用的同一个 db 句柄不会有并发安全问题吗?
不会
内部使用了 clone 字段,巧妙的实现了并发安全
具体可以移步文章:Go 源码之 gorm 并发安全机制 clone
Q7、批量插入时出现性能问题
问题现象:批量插入大量数据时,执行速度缓慢。
常见原因:未使用批量插入方法,而是循环执行单条插入;批量插入的批次大小设置不合理。
解决方案:
- 使用
CreateInBatches方法批量插入,而非循环调用Create; - 合理设置批次大小,根据数据库性能调整,一般建议每批次插入100-1000条数据;
总结
Gorm 框架作为现如今 go 开发最流行的 ORM 框架,还是很值得学习的,希望本章内容能够对开发者有所帮助
如果你对 gorm 框架的源码感兴趣,可以移步文章:Go 源码之 gorm 框架
如果在使用过程中有什么问题,欢迎大家评论区交流!!!
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!