Go的ORM框架的使用
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">特性</a></li><li><a href="#_label1">安装</a></li><li><a href="#_label2">快速入门</a></li><li><a href="#_label3">模型的定义</a></li><li><a href="#_label4">约定</a></li><ul class="second_class_ul"><li><a href="#_lab2_4_0">示例</a></li><li><a href="#_lab2_4_1">自定义约定示例</a></li><li><a href="#_lab2_4_2">gorm.Model</a></li><li><a href="#_lab2_4_3">字段级权限控制</a></li></ul><li><a href="#_label5">连接数据库</a></li><ul class="second_class_ul"></ul><li><a href="#_label6">CRUD</a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>特性</h2><ul><li>全功能 ORM</li><li>关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)</li><li>Create,Save,Update,Delete,Find 中钩子方法</li><li>支持 Preload、Joins 的预加载</li><li>事务,嵌套事务,Save Point,Rollback To Saved Point</li><li>Context、预编译模式、DryRun 模式</li><li>批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD</li><li>SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询</li><li>复合主键,索引,约束</li><li>Auto Migration</li><li>自定义 Logger</li><li>Generics API for type-safe queries and operations</li><li>Extendable, flexible plugin API: Database Resolver (multiple databases, read/write splitting) / Prometheus…</li><li>每个特性都经过了测试的重重考验</li><li>开发者友好</li></ul>
<p class="maodian"><a name="_label1"></a></p><h2>安装</h2>
<div class="jb51code"><pre class="brush:bash;">go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
</pre></div>
<p class="maodian"><a name="_label2"></a></p><h2>快速入门</h2>
<blockquote><p>基础的增删改查</p></blockquote>
<div class="jb51code"><pre class="brush:go;">package main
import (
"context"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Product struct {
gorm.Model
Codestring
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(db).Create(ctx, &Product{Code: "D42", Price: 100})
// Read
product, err := gorm.G(db).Where("id = ?", 1).First(ctx) // find product with integer primary key
products, err := gorm.G(db).Where("code = ?", "D42").Find(ctx) // find product with code D42
// Update - update product's price to 200
err = gorm.G(db).Where("id = ?", product.ID).Update(ctx, "Price", 200)
// Update - update multiple fields
err = gorm.G(db).Where("id = ?", product.ID).Updates(ctx, mapinterface{}{"Price": 200, "Code": "F42"})
// Delete - delete product
err = gorm.G(db).Where("id = ?", product.ID).Delete(ctx)
}
</pre></div>
<p class="maodian"><a name="_label3"></a></p><h2>模型的定义</h2>
<p>GORM 通过将 Go 结构体(Go structs)可以理解为Java里的Do层映射到数据库表来简化数据库交互。 了解如何在GORM中定义模型,是充分利用GORM全部功能的基础。</p>
<p>模型是使用普通结构体定义的。 这些结构体可以包含具有基本Go类型、指针或这些类型的别名,甚至是自定义类型(只需要实现 database/sql 包中的Scanner和Valuer接口)。</p>
<div class="jb51code"><pre class="brush:go;">type User struct {
ID uint // 标准字段,用作主键
Name string // 普通字符串字段
Email *string // 指向字符串的指针,允许为空(NULL)
Age uint8 // 无符号8位整数
Birthday *time.Time // 指向时间的指针,可以为空(NULL)
MemberNumber sql.NullString // 使用 sql.NullString 来处理可为空的字符串
ActivatedAtsql.NullTime // 使用 sql.NullTime 来处理可为空的时间字段
CreatedAt time.Time // GORM 自动管理的创建时间
UpdatedAt time.Time // GORM 自动管理的更新时间
ignored string // 未导出的字段会被 GORM 忽略
}
</pre></div>
<p>在此模型中:</p>
<ul><li>具体数字类型如 uint、string和 uint8 直接使用。</li><li>指向 *string 和 *time.Time 类型的指针表示可空字段。</li><li>来自 database/sql 包的 sql.NullString 和 sql.NullTime 用于具有更多控制的可空字段。</li><li>CreatedAt 和 UpdatedAt 是特殊字段,当记录被创建或更新时,GORM 会自动向内填充当前时间。</li><li>Non-exported fields (starting with a small letter) are not mapped</li></ul>
<p class="maodian"><a name="_label4"></a></p><h2>约定</h2>
<ol><li>主键:GORM 使用一个名为ID 的字段作为每个模型的默认主键。</li><li>表名:默认情况下,GORM 将结构体名称转换为 snake_case 并为表名加上复数形式。</li></ol>
<blockquote><p>例如,一个 User 结构体在数据库中对应的表名会变成 users,而 GormUserName 则会变成 gorm_user_names。</p></blockquote>
<ol start="3"><li>列名:GORM 自动将结构体字段名称转换为 snake_case 作为数据库中的列名。</li><li>时间戳字段:GORM使用字段 CreatedAt 和 UpdatedAt 来自动跟踪记录的创建和更新时间。<br />遵循这些约定可以大大减少您需要编写的配置或代码量。 但是,GORM也具有灵活性,允许根据自己的需求自定义这些设置。</li></ol>
<p class="maodian"><a name="_lab2_4_0"></a></p><h3>示例</h3>
<ol><li>列名转换</li></ol>
<table><thead><tr><th>结构体字段</th><th>数据库列名</th><th>说明</th></tr></thead><tbody><tr><td>ID</td><td>id</td><td>默认主键</td></tr><tr><td>Name</td><td>name</td><td>转换为 snake_case</td></tr><tr><td>Email</td><td>email</td><td>转换为 snake_case</td></tr><tr><td>Age</td><td>age</td><td>转换为 snake_case</td></tr><tr><td>CreatedAt</td><td>created_at</td><td>时间戳字段</td></tr><tr><td>UpdatedAt</td><td>updated_at</td><td>时间戳字段</td></tr></tbody></table>
<ol start="2"><li>表名转换</li></ol>
<table><thead><tr><th>结构体名称</th><th>数据库表名</th><th>说明</th></tr></thead><tbody><tr><td>User</td><td>users</td><td>结构体名转 snake_case + 复数</td></tr><tr><td>GormUserName</td><td>gorm_user_names</td><td>结构体名转 snake_case + 复数</td></tr><tr><td>Product</td><td>products</td><td>结构体名转 snake_case + 复数</td></tr><tr><td>OrderItem</td><td>order_items</td><td>结构体名转 snake_case + 复数</td></tr></tbody></table>
<p class="maodian"><a name="_lab2_4_1"></a></p><h3>自定义约定示例</h3>
<p>如果需要自定义表名,可以实现TableName方法:</p>
<div class="jb51code"><pre class="brush:go;">func (User) TableName() string {
return "user_profiles" // 自定义表名
}
</pre></div>
<p>或者通过配置全局修改:</p>
<div class="jb51code"><pre class="brush:go;">db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "tbl_", // 表名前缀
SingularTable: true, // 使用单数表名
NoLowerCase: false,// 使用小写
NameReplacer: strings.NewReplacer("ID", "Id"), // 替换特定名称
},
})
</pre></div>
<p class="maodian"><a name="_lab2_4_2"></a></p><h3>gorm.Model</h3>
<p>GORM提供了一个预定义的结构体,名为gorm.Model,其中包含常用字段:</p>
<div class="jb51code"><pre class="brush:go;">// gorm.Model 的定义
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
</pre></div>
<ul><li>ID :每个记录的唯一标识符(主键)。</li><li>CreatedAt :在创建记录时自动设置为当前时间。</li><li>UpdatedAt:每当记录更新时,自动更新为当前时间。</li><li>DeletedAt:用于软删除(将记录标记为已删除,而实际上并未从数据库中删除)。<br />使用方式如下:</li></ul>
<div class="jb51code"><pre class="brush: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 来处理可为空的字符串
ActivatedAtsql.NullTime // 使用 sql.NullTime 来处理可为空的时间字段
ignored string // 未导出的字段会被 GORM 忽略
}
</pre></div>
<p class="maodian"><a name="_lab2_4_3"></a></p><h3>字段级权限控制</h3>
<p>可导出的字段在使用 GORM 进行 CRUD 时拥有全部的权限,此外,GORM 允许您用标签控制字段级别的权限。这样您就可以让一个字段的权限是只读、只写、只创建、只更新或者被忽略</p>
<div class="jb51code"><pre class="brush: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 迁移会忽略该字段
}
</pre></div>
<p>嵌入结构体</p>
<ol><li>方式1:直接嵌入</li></ol>
<div class="jb51code"><pre class="brush:go;">type Author struct {
Namestring
Email string
}
type Blog struct {
Author
ID int
Upvotes int32
}
// equals
type Blog struct {
ID int64
Name string
Email string
Upvotes int32
}
</pre></div>
<ol start="2"><li>通过标签 embedded嵌入</li></ol>
<div class="jb51code"><pre class="brush:go;">type Author struct {
Namestring
Email string
}
type Blog struct {
ID int
AuthorAuthor `gorm:"embedded"`
Upvotes int32
}
// 等效于
type Blog struct {
ID int64
Namestring
Email string
Upvotesint32
}
</pre></div>
<ol start="3"><li>使用标签 embeddedPrefix 来为 db 中的字段名添加前缀</li></ol>
<div class="jb51code"><pre class="brush:go;">type Blog struct {
ID int
AuthorAuthor `gorm:"embedded;embeddedPrefix:author_"`
Upvotes int32
}
// 等效于
type Blog struct {
ID int64
AuthorName string
AuthorEmail string
Upvotes int32
}
</pre></div>
<p>声明模型时,标签是可选的,GORM支持以下标签:标签不区分大小写,但建议使用驼峰式命名法(camelCase)。如果使用多个标签,则应使用分号(;)分隔。对于解析器具有特殊含义的字符可以使用反斜杠(1)进行转义,以便将其用作参数值。</p>
<table><thead><tr><th>标签名</th><th>说明</th></tr></thead><tbody><tr><td>column</td><td>指定 db 列名</td></tr><tr><td>type</td><td>列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes,并且可以和其他标签一起使用,例如:not null、size、autoIncrement … 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT</td></tr><tr><td>serializer</td><td>指定将数据序列化或反序列化到数据库中的序列化器,例如:serializer:json/gob/unixtime</td></tr><tr><td>size</td><td>定义列数据类型的大小或长度,例如:size: 256</td></tr><tr><td>primaryKey</td><td>将列定义为主键</td></tr><tr><td>unique</td><td>将列定义为唯一键</td></tr><tr><td>default</td><td>定义列的默认值</td></tr><tr><td>precision</td><td>指定列的精度</td></tr><tr><td>scale</td><td>指定列大小</td></tr><tr><td>not null</td><td>指定列为 NOT NULL</td></tr><tr><td>autoIncrement</td><td>指定列为自动增长</td></tr><tr><td>autoIncrementIncrement</td><td>自动步长,控制连续记录之间的间隔</td></tr><tr><td>embedded</td><td>嵌套字段</td></tr><tr><td>embeddedPrefix</td><td>嵌入字段的列名前缀</td></tr><tr><td>autoCreateTime</td><td>创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano / milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano</td></tr><tr><td>autoUpdateTime</td><td>创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano / milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli</td></tr><tr><td>index</td><td>根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 <a href="#%E7%B4%A2%E5%BC%95" rel="external nofollow" >索引</a> 获取详情</td></tr><tr><td>uniqueIndex</td><td>与 index 相同,但创建的是唯一索引</td></tr><tr><td>check</td><td>创建检查约束,例如 check:age > 13,查看 <a href="#%E7%BA%A6%E6%9D%9F" rel="external nofollow" >约束</a> 获取详情</td></tr><tr><td><-</td><td>设置字段写入的权限,<-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限</td></tr><tr><td>-></td><td>设置字段读的权限,->:false 无读权限</td></tr><tr><td>-</td><td>忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限</td></tr><tr><td>comment</td><td>迁移时为字段添加注释</td></tr></tbody></table>
<p class="maodian"><a name="_label5"></a></p><h2>连接数据库</h2>
<p>最基础的用法</p>
<div class="jb51code"><pre class="brush: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{})
}
</pre></div>
<p class="maodian"><a name="_label6"></a></p><h2>CRUD</h2>
<div class="jb51code"><pre class="brush: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 // 处理可为空的字符串
ActivatedAtsql.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"},
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)
}
</pre></div>
<p>在使用 GORM 的 AutoMigrate 功能时,无需手动创建数据库表,只需定义好 Go 结构体(模型),然后调用 AutoMigrate 方法,GORM 会自动根据结构体定义创建数据库表。</p>
<p>方式2:</p>
<div class="jb51code"><pre class="brush: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 mapinterface{} `gorm:"type:json"` // 使用JSON存储参数
CreatedAttime.Time
UpdatedAttime.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: mapinterface{}{"name": "张三", "age": 30},
}
db.Create(&robot)
// 4. 演示您的示例:指定表名 + 条件更新
entityId := uint(1001)
bizType := "user_profile"
params := mapinterface{}{
"params": mapinterface{}{"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: mapinterface{}{"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(mapinterface{}{
"params": mapinterface{}{"status": "active"},
})
// 6.6 原生SQL查询
var rawResult []struct {
EntityID uint
BizTypestring
}
db.Raw("SELECT entity_id, biz_type FROM entity_robot WHERE params->>'$.age' > ?", 30).Scan(&rawResult)
fmt.Printf("查询到 %d 条年龄>30的记录\n", len(rawResult))
}
</pre></div>
<p>这种方式也是我们项目中常用的一个操作</p>
頁:
[1]