眼里容不得沙子 發表於 2026-1-13 08:32:33

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&hellip;</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"), &amp;gorm.Config{})
if err != nil {
    panic("failed to connect database")
}

ctx := context.Background()

// Migrate the schema
db.AutoMigrate(&amp;Product{})

// Create
err = gorm.G(db).Create(ctx, &amp;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), &amp;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:"&lt;-:create"` // 允许读和创建
Name string `gorm:"&lt;-:update"` // 允许读和更新
Name string `gorm:"&lt;-"`      // 允许读和写(创建和更新)
Name string `gorm:"&lt;-:false"`// 允许读,禁止写
Name string `gorm:"-&gt;"`      // 只读(除非有自定义配置,否则禁止写)
Name string `gorm:"-&gt;;&lt;-:create"` // 允许读和写
Name string `gorm:"-&gt;:false;&lt;-: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 &hellip; 像 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 &gt; 13,查看 <a href="#%E7%BA%A6%E6%9D%9F" rel="external nofollow" >约束</a> 获取详情</td></tr><tr><td>&lt;-</td><td>设置字段写入的权限,&lt;-:create 只创建、&lt;-:update 只更新、&lt;-:false 无写入权限、&lt;- 创建和更新权限</td></tr><tr><td>-&gt;</td><td>设置字段读的权限,-&gt;: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&amp;parseTime=True&amp;loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &amp;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&amp;parseTime=True&amp;loc=Local"
      db, err := gorm.Open(mysql.Open(dsn), &amp;gorm.Config{})
      if err != nil {
                panic("数据库连接失败: " + err.Error())
      }

      // 2. 自动迁移(创建表)
      db.AutoMigrate(&amp;User{})

      // 3. 创建操作 (Create)
      fmt.Println("\n=== 创建用户 ===")
      user1 := User{
                Name:      "张三",
                Email:   &amp;[]string{"zhangsan@example.com"},
                Age:       30,
                Birthday:&amp;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(&amp;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(&amp;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 &gt; ?", 25).Find(&amp;users)
      fmt.Printf("年龄大于25的用户: %d 个\n", len(users))

      // 使用条件查询
      var user3 User
      db.Where("name = ?", "张三").First(&amp;user3)
      fmt.Printf("条件查询: %s (ID: %d)\n", user3.Name, user3.ID)

      // 5. 更新操作 (Update)
      fmt.Println("\n=== 更新用户 ===")
      user2.Age = 31
      if err := db.Save(&amp;user2).Error; err != nil {
                panic("更新用户失败: " + err.Error())
      }
      fmt.Printf("更新成功! 年龄: %d\n", user2.Age)

      // 更新特定字段
      db.Model(&amp;user2).Update("age", 32)
      fmt.Printf("更新特定字段: 年龄: %d\n", user2.Age)

      // 6. 删除操作 (Delete)
      fmt.Println("\n=== 删除用户 ===")
      // 软删除(默认)
      if err := db.Delete(&amp;user2).Error; err != nil {
                panic("软删除失败: " + err.Error())
      }
      fmt.Printf("软删除成功! ID: %d\n", user2.ID)

      // 硬删除(需要设置 DeleteMode 为 HardDelete)
      if err := db.Unscoped().Delete(&amp;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(&amp;user4, User{Name: "李四"})
      fmt.Printf("创建或更新: %s (ID: %d)\n", user4.Name, user4.ID)

      // 通过 ID 查询
      var user5 User
      db.First(&amp;user5, 1) // 通过 ID 查询
      fmt.Printf("通过 ID 查询: %s\n", user5.Name)

      // 分页查询
      var usersPage []User
      db.Limit(2).Offset(0).Find(&amp;usersPage)
      fmt.Printf("分页查询: %d 个用户\n", len(usersPage))

      // 计数
      var count int64
      db.Model(&amp;User{}).Where("age &gt; ?", 25).Count(&amp;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&amp;parseTime=True&amp;loc=Local"
      db, err := gorm.Open(mysql.Open(dsn), &amp;gorm.Config{})
      if err != nil {
                panic("数据库连接失败: " + err.Error())
      }

      // 2. 自动迁移(创建表)
      db.AutoMigrate(&amp;EntityRobot{})

      // 3. 创建测试数据
      robot := EntityRobot{
                EntityID: 1001,
                BizType:"user_profile",
                Params:   mapinterface{}{"name": "张三", "age": 30},
      }
      db.Create(&amp;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(&amp;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(&amp;newRobot)

      // 6.2 查询(带条件)
      var robots []EntityRobot
      db.Where("biz_type = ?", "order").Find(&amp;robots)
      fmt.Printf("查询到 %d 个订单记录\n", len(robots))

      // 6.3 软删除(默认)
      db.Delete(&amp;newRobot)

      // 6.4 硬删除(需要使用 Unscoped)
      db.Unscoped().Delete(&amp;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-&gt;&gt;'$.age' &gt; ?", 30).Scan(&amp;rawResult)
      fmt.Printf("查询到 %d 条年龄&gt;30的记录\n", len(rawResult))
}
</pre></div>
<p>这种方式也是我们项目中常用的一个操作</p>
頁: [1]
查看完整版本: Go的ORM框架的使用