目录

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映射数据库字段名,解决结构体字段名与数据库字段名不一致问题;autoCreateTimeautoUpdateTime实现时间自动填充;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事务使用BeginCommitRollback方法实现:

// 事务示例:创建用户并创建关联订单
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 string gorm:“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 框架

如果在使用过程中有什么问题,欢迎大家评论区交流!!!

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-gorm-guide/

备用原文链接: https://blog.fiveyoboy.com/articles/go-gorm-guide/