文章目录
特性
- 全功能 ORM
- 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
- Create,Save,Update,Delete,Find 中钩子方法
- 支持 Preload、Joins 的预加载
- 事务,嵌套事务,Save Point,Rollback To Saved Point
- Context、预编译模式、DryRun 模式
- 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
- SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
- 复合主键,索引,约束
- Auto Migration
- 自定义 Logger
- Generics API for type-safe queries and operations
- Extendable, flexible plugin API: Database Resolver (multiple databases, read/write splitting) / Prometheus...
- 每个特性都经过了测试的重重考验
- 开发者友好
安装
go
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
快速入门
基础的增删改查
go
package main
import (
"context"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Product struct {
gorm.Model
Code string
Price uint
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
ctx := context.Background()
// Migrate the schema
db.AutoMigrate(&Product{})
// Create
err = gorm.G[Product](db).Create(ctx, &Product{Code: "D42", Price: 100})
// Read
product, err := gorm.G[Product](db).Where("id = ?", 1).First(ctx) // find product with integer primary key
products, err := gorm.G[Product](db).Where("code = ?", "D42").Find(ctx) // find product with code D42
// Update - update product's price to 200
err = gorm.G[Product](db).Where("id = ?", product.ID).Update(ctx, "Price", 200)
// Update - update multiple fields
err = gorm.G[Product](db).Where("id = ?", product.ID).Updates(ctx, map[string]interface{}{"Price": 200, "Code": "F42"})
// Delete - delete product
err = gorm.G[Product](db).Where("id = ?", product.ID).Delete(ctx)
}
模型的定义
GORM 通过将 Go 结构体(Go structs)可以理解为Java里的Do层映射到数据库表来简化数据库交互。 了解如何在GORM中定义模型,是充分利用GORM全部功能的基础。
模型是使用普通结构体定义的。 这些结构体可以包含具有基本Go类型、指针或这些类型的别名,甚至是自定义类型(只需要实现 database/sql 包中的Scanner和Valuer接口)。
go
type User struct {
ID uint // 标准字段,用作主键
Name string // 普通字符串字段
Email *string // 指向字符串的指针,允许为空(NULL)
Age uint8 // 无符号8位整数
Birthday *time.Time // 指向时间的指针,可以为空(NULL)
MemberNumber sql.NullString // 使用 sql.NullString 来处理可为空的字符串
ActivatedAt sql.NullTime // 使用 sql.NullTime 来处理可为空的时间字段
CreatedAt time.Time // GORM 自动管理的创建时间
UpdatedAt time.Time // GORM 自动管理的更新时间
ignored string // 未导出的字段会被 GORM 忽略
}
在此模型中:
- 具体数字类型如 uint、string和 uint8 直接使用。
- 指向 *string 和 *time.Time 类型的指针表示可空字段。
- 来自 database/sql 包的 sql.NullString 和 sql.NullTime 用于具有更多控制的可空字段。
- CreatedAt 和 UpdatedAt 是特殊字段,当记录被创建或更新时,GORM 会自动向内填充当前时间。
- Non-exported fields (starting with a small letter) are not mapped
约定
- 主键:GORM 使用一个名为ID 的字段作为每个模型的默认主键。
- 表名:默认情况下,GORM 将结构体名称转换为 snake_case 并为表名加上复数形式。
例如,一个 User 结构体在数据库中对应的表名会变成 users,而 GormUserName 则会变成 gorm_user_names。
- 列名:GORM 自动将结构体字段名称转换为 snake_case 作为数据库中的列名。
- 时间戳字段:GORM使用字段 CreatedAt 和 UpdatedAt 来自动跟踪记录的创建和更新时间。
遵循这些约定可以大大减少您需要编写的配置或代码量。 但是,GORM也具有灵活性,允许根据自己的需求自定义这些设置。
示例
- 列名转换
| 结构体字段 | 数据库列名 | 说明 |
|---|---|---|
| ID | id | 默认主键 |
| Name | name | 转换为 snake_case |
| 转换为 snake_case | ||
| Age | age | 转换为 snake_case |
| CreatedAt | created_at | 时间戳字段 |
| UpdatedAt | updated_at | 时间戳字段 |
- 表名转换
| 结构体名称 | 数据库表名 | 说明 |
|---|---|---|
| User | users | 结构体名转 snake_case + 复数 |
| GormUserName | gorm_user_names | 结构体名转 snake_case + 复数 |
| Product | products | 结构体名转 snake_case + 复数 |
| OrderItem | order_items | 结构体名转 snake_case + 复数 |
自定义约定示例
如果需要自定义表名,可以实现TableName方法:
go
func (User) TableName() string {
return "user_profiles" // 自定义表名
}
或者通过配置全局修改:
go
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "tbl_", // 表名前缀
SingularTable: true, // 使用单数表名
NoLowerCase: false, // 使用小写
NameReplacer: strings.NewReplacer("ID", "Id"), // 替换特定名称
},
})
gorm.Model
GORM提供了一个预定义的结构体,名为gorm.Model,其中包含常用字段:
go
// gorm.Model 的定义
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
- ID :每个记录的唯一标识符(主键)。
- CreatedAt :在创建记录时自动设置为当前时间。
- UpdatedAt:每当记录更新时,自动更新为当前时间。
- DeletedAt:用于软删除(将记录标记为已删除,而实际上并未从数据库中删除)。
使用方式如下:
go
package model
import (
"database/sql"
"time"
"gorm.io/gorm"
)
type User struct {
gorm.Model
ID uint // 标准字段,用作主键
Name string // 普通字符串字段
Email *string // 指向字符串的指针,允许为空(NULL)
Age uint8 // 无符号8位整数
Birthday *time.Time // 指向时间的指针,可以为空(NULL)
MemberNumber sql.NullString // 使用 sql.NullString 来处理可为空的字符串
ActivatedAt sql.NullTime // 使用 sql.NullTime 来处理可为空的时间字段
ignored string // 未导出的字段会被 GORM 忽略
}
字段级权限控制
可导出的字段在使用 GORM 进行 CRUD 时拥有全部的权限,此外,GORM 允许您用标签控制字段级别的权限。这样您就可以让一个字段的权限是只读、只写、只创建、只更新或者被忽略
go
type User struct {
Name string `gorm:"<-:create"` // 允许读和创建
Name string `gorm:"<-:update"` // 允许读和更新
Name string `gorm:"<-"` // 允许读和写(创建和更新)
Name string `gorm:"<-:false"` // 允许读,禁止写
Name string `gorm:"->"` // 只读(除非有自定义配置,否则禁止写)
Name string `gorm:"->;<-:create"` // 允许读和写
Name string `gorm:"->:false;<-:create"` // 仅创建(禁止从 db 读)
Name string `gorm:"-"` // 通过 struct 读写会忽略该字段
Name string `gorm:"-:all"` // 通过 struct 读写、迁移会忽略该字段
Name string `gorm:"-:migration"` // 通过 struct 迁移会忽略该字段
}
嵌入结构体
- 方式1:直接嵌入
go
type Author struct {
Name string
Email string
}
type Blog struct {
Author
ID int
Upvotes int32
}
// equals
type Blog struct {
ID int64
Name string
Email string
Upvotes int32
}
- 通过标签 embedded嵌入
go
type Author struct {
Name string
Email string
}
type Blog struct {
ID int
Author Author `gorm:"embedded"`
Upvotes int32
}
// 等效于
type Blog struct {
ID int64
Name string
Email string
Upvotes int32
}
- 使用标签 embeddedPrefix 来为 db 中的字段名添加前缀
go
type Blog struct {
ID int
Author Author `gorm:"embedded;embeddedPrefix:author_"`
Upvotes int32
}
// 等效于
type Blog struct {
ID int64
AuthorName string
AuthorEmail string
Upvotes int32
}
声明模型时,标签是可选的,GORM支持以下标签:标签不区分大小写,但建议使用驼峰式命名法(camelCase)。如果使用多个标签,则应使用分号(;)分隔。对于解析器具有特殊含义的字符可以使用反斜杠(1)进行转义,以便将其用作参数值。
| 标签名 | 说明 |
|---|---|
column |
指定 db 列名 |
type |
列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes,并且可以和其他标签一起使用,例如:not null、size、autoIncrement ... 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT |
serializer |
指定将数据序列化或反序列化到数据库中的序列化器,例如:serializer:json/gob/unixtime |
size |
定义列数据类型的大小或长度,例如:size: 256 |
primaryKey |
将列定义为主键 |
unique |
将列定义为唯一键 |
default |
定义列的默认值 |
precision |
指定列的精度 |
scale |
指定列大小 |
not null |
指定列为 NOT NULL |
autoIncrement |
指定列为自动增长 |
autoIncrementIncrement |
自动步长,控制连续记录之间的间隔 |
embedded |
嵌套字段 |
embeddedPrefix |
嵌入字段的列名前缀 |
autoCreateTime |
创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano / milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano |
autoUpdateTime |
创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano / milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli |
index |
根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 [索引](#标签名 说明 column 指定 db 列名 type 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes,并且可以和其他标签一起使用,例如:not null、size、autoIncrement … 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT serializer 指定将数据序列化或反序列化到数据库中的序列化器,例如:serializer:json/gob/unixtime size 定义列数据类型的大小或长度,例如:size: 256 primaryKey 将列定义为主键 unique 将列定义为唯一键 default 定义列的默认值 precision 指定列的精度 scale 指定列大小 not null 指定列为 NOT NULL autoIncrement 指定列为自动增长 autoIncrementIncrement 自动步长,控制连续记录之间的间隔 embedded 嵌套字段 embeddedPrefix 嵌入字段的列名前缀 autoCreateTime 创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano / milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano autoUpdateTime 创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano / milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli index 根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情 uniqueIndex 与 index 相同,但创建的是唯一索引 check 创建检查约束,例如 check:age > 13,查看 约束 获取详情 <- 设置字段写入的权限,<-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限 -> 设置字段读的权限,->:false 无读权限 - 忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限 comment 迁移时为字段添加注释) 获取详情 |
uniqueIndex |
与 index 相同,但创建的是唯一索引 |
check |
创建检查约束,例如 check:age > 13,查看 [约束](#标签名 说明 column 指定 db 列名 type 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes,并且可以和其他标签一起使用,例如:not null、size、autoIncrement … 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT serializer 指定将数据序列化或反序列化到数据库中的序列化器,例如:serializer:json/gob/unixtime size 定义列数据类型的大小或长度,例如:size: 256 primaryKey 将列定义为主键 unique 将列定义为唯一键 default 定义列的默认值 precision 指定列的精度 scale 指定列大小 not null 指定列为 NOT NULL autoIncrement 指定列为自动增长 autoIncrementIncrement 自动步长,控制连续记录之间的间隔 embedded 嵌套字段 embeddedPrefix 嵌入字段的列名前缀 autoCreateTime 创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano / milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano autoUpdateTime 创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano / milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli index 根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情 uniqueIndex 与 index 相同,但创建的是唯一索引 check 创建检查约束,例如 check:age > 13,查看 约束 获取详情 <- 设置字段写入的权限,<-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限 -> 设置字段读的权限,->:false 无读权限 - 忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限 comment 迁移时为字段添加注释) 获取详情 |
<- |
设置字段写入的权限,<-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限 |
-> |
设置字段读的权限,->:false 无读权限 |
- |
忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限 |
comment |
迁移时为字段添加注释 |
连接数据库
最基础的用法
go
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}
CRUD
go
package main
import (
"fmt"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// 定义符合 GORM 约定的 User 模型(嵌入 gorm.Model)
type User struct {
gorm.Model // 自动包含 ID, CreatedAt, UpdatedAt, DeletedAt
Name string `gorm:"size:255"` // 可指定字段长度
Email *string `gorm:"unique"` // 唯一索引
Age uint8 `gorm:"default:18"` // 默认值
Birthday *time.Time // 指向时间的指针
MemberNumber sql.NullString // 处理可为空的字符串
ActivatedAt sql.NullTime // 处理可为空的时间字段
ignored string // 未导出字段会被 GORM 忽略
}
func main() {
// 1. 连接 MySQL 数据库
dsn := "root:root@tcp(127.0.0.1:3306)/gorm_demo?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("数据库连接失败: " + err.Error())
}
// 2. 自动迁移(创建表)
db.AutoMigrate(&User{})
// 3. 创建操作 (Create)
fmt.Println("\n=== 创建用户 ===")
user1 := User{
Name: "张三",
Email: &[]string{"zhangsan@example.com"}[0],
Age: 30,
Birthday: &time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
MemberNumber: sql.NullString{String: "M12345", Valid: true},
ActivatedAt: sql.NullTime{Time: time.Now(), Valid: true},
}
if err := db.Create(&user1).Error; err != nil {
panic("创建用户失败: " + err.Error())
}
fmt.Printf("创建成功! ID: %d\n", user1.ID)
// 4. 查询操作 (Read)
fmt.Println("\n=== 查询用户 ===")
var user2 User
// 查询单个记录
if err := db.First(&user2, user1.ID).Error; err != nil {
panic("查询用户失败: " + err.Error())
}
fmt.Printf("查询到用户: %s (ID: %d, 年龄: %d)\n", user2.Name, user2.ID, user2.Age)
// 查询多个记录
var users []User
db.Where("age > ?", 25).Find(&users)
fmt.Printf("年龄大于25的用户: %d 个\n", len(users))
// 使用条件查询
var user3 User
db.Where("name = ?", "张三").First(&user3)
fmt.Printf("条件查询: %s (ID: %d)\n", user3.Name, user3.ID)
// 5. 更新操作 (Update)
fmt.Println("\n=== 更新用户 ===")
user2.Age = 31
if err := db.Save(&user2).Error; err != nil {
panic("更新用户失败: " + err.Error())
}
fmt.Printf("更新成功! 年龄: %d\n", user2.Age)
// 更新特定字段
db.Model(&user2).Update("age", 32)
fmt.Printf("更新特定字段: 年龄: %d\n", user2.Age)
// 6. 删除操作 (Delete)
fmt.Println("\n=== 删除用户 ===")
// 软删除(默认)
if err := db.Delete(&user2).Error; err != nil {
panic("软删除失败: " + err.Error())
}
fmt.Printf("软删除成功! ID: %d\n", user2.ID)
// 硬删除(需要设置 DeleteMode 为 HardDelete)
if err := db.Unscoped().Delete(&user2).Error; err != nil {
panic("硬删除失败: " + err.Error())
}
fmt.Printf("硬删除成功! ID: %d\n", user2.ID)
// 7. 其他常用操作
fmt.Println("\n=== 其他常用操作 ===")
// 创建或更新(如果存在则更新,不存在则创建)
user4 := User{Name: "李四", Age: 25}
db.FirstOrCreate(&user4, User{Name: "李四"})
fmt.Printf("创建或更新: %s (ID: %d)\n", user4.Name, user4.ID)
// 通过 ID 查询
var user5 User
db.First(&user5, 1) // 通过 ID 查询
fmt.Printf("通过 ID 查询: %s\n", user5.Name)
// 分页查询
var usersPage []User
db.Limit(2).Offset(0).Find(&usersPage)
fmt.Printf("分页查询: %d 个用户\n", len(usersPage))
// 计数
var count int64
db.Model(&User{}).Where("age > ?", 25).Count(&count)
fmt.Printf("年龄大于25的用户总数: %d\n", count)
}
在使用 GORM 的 AutoMigrate 功能时,无需手动创建数据库表,只需定义好 Go 结构体(模型),然后调用 AutoMigrate 方法,GORM 会自动根据结构体定义创建数据库表。
方式2:
go
package main
import (
"fmt"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// 自定义表名模型(演示如何指定表名)
type EntityRobot struct {
ID uint `gorm:"primary_key"`
EntityID uint `gorm:"index:idx_entity_biz"`
BizType string `gorm:"size:50;index:idx_entity_biz"`
Params map[string]interface{} `gorm:"type:json"` // 使用JSON存储参数
CreatedAt time.Time
UpdatedAt time.Time
}
// 实现TableName方法指定表名
func (EntityRobot) TableName() string {
return "entity_robot" // 自定义表名,与数据库表名一致
}
func main() {
// 1. 数据库连接
dsn := "root:root@tcp(127.0.0.1:3306)/gorm_demo?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("数据库连接失败: " + err.Error())
}
// 2. 自动迁移(创建表)
db.AutoMigrate(&EntityRobot{})
// 3. 创建测试数据
robot := EntityRobot{
EntityID: 1001,
BizType: "user_profile",
Params: map[string]interface{}{"name": "张三", "age": 30},
}
db.Create(&robot)
// 4. 演示您的示例:指定表名 + 条件更新
entityId := uint(1001)
bizType := "user_profile"
params := map[string]interface{}{
"params": map[string]interface{}{"name": "李四", "age": 32},
}
// 使用 Table() 指定表名 + Where 条件 + Update
result := db.Table("entity_robot").
Where("entity_id = ? AND biz_type = ?", entityId, bizType).
Updates(params)
// 检查更新结果
fmt.Printf("更新影响行数: %d\n", result.RowsAffected)
if result.Error != nil {
panic("更新失败: " + result.Error.Error())
}
// 5. 验证更新结果
var updatedRobot EntityRobot
db.Where("entity_id = ? AND biz_type = ?", entityId, bizType).First(&updatedRobot)
fmt.Printf("更新后参数: %+v\n", updatedRobot.Params)
// 6. 其他常用操作演示
// 6.1 创建(带JSON参数)
newRobot := EntityRobot{
EntityID: 1002,
BizType: "order",
Params: map[string]interface{}{"order_id": "ORD1001", "amount": 199.9},
}
db.Create(&newRobot)
// 6.2 查询(带条件)
var robots []EntityRobot
db.Where("biz_type = ?", "order").Find(&robots)
fmt.Printf("查询到 %d 个订单记录\n", len(robots))
// 6.3 软删除(默认)
db.Delete(&newRobot)
// 6.4 硬删除(需要使用 Unscoped)
db.Unscoped().Delete(&newRobot)
// 6.5 批量更新(更新多个记录)
db.Table("entity_robot").
Where("biz_type = ?", "user_profile").
Updates(map[string]interface{}{
"params": map[string]interface{}{"status": "active"},
})
// 6.6 原生SQL查询
var rawResult []struct {
EntityID uint
BizType string
}
db.Raw("SELECT entity_id, biz_type FROM entity_robot WHERE params->>'$.age' > ?", 30).Scan(&rawResult)
fmt.Printf("查询到 %d 条年龄>30的记录\n", len(rawResult))
}
这种方式也是我们项目中常用的一个操作