2026-04-28 20:02:42 +08:00
|
|
|
|
---
|
|
|
|
|
|
tags: [GORM, Go, ORM, 模型, struct tag, 字段映射, 主键, 表名]
|
|
|
|
|
|
create time: 2026-04-28 00:00
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 模型定义
|
|
|
|
|
|
|
|
|
|
|
|
## 概述
|
|
|
|
|
|
|
|
|
|
|
|
模型(Model)是 GORM 操作的核心入口——一个普通的 Go struct 被 GORM「看见」后,就可以直接进行数据库操作。本章讲解如何通过 struct tag、命名约定和接口来自定义 GORM 对模型的解读。
|
|
|
|
|
|
|
|
|
|
|
|
## 什么是 Model?
|
|
|
|
|
|
|
|
|
|
|
|
> [!definition] Model
|
|
|
|
|
|
> Model 是一个包含以下任一条件的 struct:
|
|
|
|
|
|
> 1. 有主键字段
|
|
|
|
|
|
> 2. 定义了 `TableName()` 方法
|
|
|
|
|
|
> 3. 被 `AutoMigrate`、`Create`、`Where` 等方法引用
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type User struct {
|
|
|
|
|
|
ID uint `gorm:"primaryKey"`
|
|
|
|
|
|
Name string `gorm:"size:64;not null"`
|
|
|
|
|
|
}
|
|
|
|
|
|
// 这就是一个最简单的 GORM Model
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-28 20:12:15 +08:00
|
|
|
|
> [!tip] 核心原则
|
|
|
|
|
|
> GORM 的本质是一个「约定优于配置」的 ORM。struct 只是普通的 Go 结构体,`gorm:"..."` tag 告诉 GORM 如何将它映射到数据库——不修改运行时行为,只在构建 SQL 和建表时生效。这意味着你可以将同一个 struct 同时用于 HTTP 请求体和数据库模型(通过暴露合适的字段)。
|
|
|
|
|
|
|
2026-04-28 20:02:42 +08:00
|
|
|
|
## 表名规则
|
|
|
|
|
|
|
2026-05-05 20:22:48 +08:00
|
|
|
|
GORM 通过三层优先级推导表名:`db.Table()` → `TableName()` 方法 → struct 名蛇形复数。自定义表名时还涉及 `NamingStrategy` 全局配置、value/pointer receiver 选择、分表策略等细节——详见子文档:
|
2026-04-28 20:02:42 +08:00
|
|
|
|
|
2026-05-05 20:22:48 +08:00
|
|
|
|
> [!abstract] 子文档
|
|
|
|
|
|
> - [[02-模型定义/表名规则]] — 默认策略、TableName()、db.Table()、NamingStrategy、分表实战、决策指南
|
2026-04-28 20:02:42 +08:00
|
|
|
|
|
|
|
|
|
|
## 字段 Tag 详解
|
|
|
|
|
|
|
|
|
|
|
|
### 完整 Tag 语法
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
gorm:"column:name;type:bigint;not null;default:0;uniqueIndex;index;comment:用户ID"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
| Tag 关键字 | 作用 | 示例 |
|
|
|
|
|
|
|------------|------|------|
|
|
|
|
|
|
| `column` | 映射列名 | `column:user_id` |
|
|
|
|
|
|
| `type` | 列类型覆盖 | `type:varchar(128)` |
|
|
|
|
|
|
| `not null` | NOT NULL 约束 | `not null` |
|
|
|
|
|
|
| `default` | 默认值 | `default:'unknown'` |
|
|
|
|
|
|
| `primaryKey` | 主键 | `primaryKey` |
|
|
|
|
|
|
| `autoIncrement` | 自增 | `autoIncrement` |
|
|
|
|
|
|
| `uniqueIndex` | 唯一索引 | `uniqueIndex:idx_name` |
|
|
|
|
|
|
| `index` | 普通索引 | `index:idx_status` |
|
|
|
|
|
|
| `comment` | 注释(MySQL) | `comment:用户名` |
|
|
|
|
|
|
| `<-` | 字段写入控制 | `<-:true` / `<-:false` / `<-:create` / `<-:update` |
|
2026-04-28 20:12:15 +08:00
|
|
|
|
| `->` | 读取控制(只读 = 只从 DB 读出) | `->:true` / `->:false`(不读取回 struct) |
|
|
|
|
|
|
| `<-` + `->` | 组合控制读写 | `<-:false;->:true`(只读,不可写入) |
|
2026-04-28 20:02:42 +08:00
|
|
|
|
| `-` | 忽略此字段 | `-` |
|
|
|
|
|
|
|
|
|
|
|
|
### 写入权限控制
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type Product struct {
|
|
|
|
|
|
ID uint `gorm:"primaryKey;autoIncrement"`
|
|
|
|
|
|
Name string `gorm:"not null;<-:create"` // 仅创建时可写入
|
|
|
|
|
|
Price float64 `gorm:"not null;<-:true;->:true"` // 读写均可(默认)
|
|
|
|
|
|
CreatedAt time.Time `gorm:"<-:create;->:true;autoTime"` // 仅创建时自动填充
|
|
|
|
|
|
UpdatedAt time.Time `gorm:"<-:update;->:true;autoTime"` // 仅更新时自动填充
|
|
|
|
|
|
ViewCount int `gorm:"->:false;<-:false"` // 完全忽略(非 DB 字段)
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
> [!example] 方向记忆法
|
|
|
|
|
|
> `<-` 表示数据**流向数据库**(写入),`->` 表示数据**从数据库流出**(读取)。箭头方向就是数据的方向。
|
|
|
|
|
|
|
2026-04-28 20:12:15 +08:00
|
|
|
|
### 嵌入 Struct(组合优于继承)
|
|
|
|
|
|
|
|
|
|
|
|
Go 不支持类的继承,但可以通过结构体嵌套实现代码复用:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// 方式一:显式嵌入公共字段
|
|
|
|
|
|
type AuditRecord struct {
|
|
|
|
|
|
CreatedBy string `gorm:"size:64"` // 创建人
|
|
|
|
|
|
UpdatedBy string `gorm:"size:64"` // 最后修改人
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type User struct {
|
|
|
|
|
|
ID uint `gorm:"primaryKey"`
|
|
|
|
|
|
Name string `gorm:"size:64;not null"`
|
|
|
|
|
|
AuditRecord // 匿名嵌入——Go 将 AuditRecord 的字段提升为 User 的直接字段
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 方式二:使用 gorm.Model(推荐 🌟)
|
|
|
|
|
|
type Article struct {
|
|
|
|
|
|
gorm.Model // 自动包含 ID、CreatedAt、UpdatedAt、DeletedAt
|
|
|
|
|
|
Title string `gorm:"size:128;not null"`
|
|
|
|
|
|
Content string `gorm:"type:text"`
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
> [!note] 为什么推荐 `gorm.Model`?
|
|
|
|
|
|
> `gorm.Model` 是 GORM 预定义的 struct,包含四个最常见字段:
|
|
|
|
|
|
> ```go
|
|
|
|
|
|
> type Model struct {
|
|
|
|
|
|
> ID any `gorm:"primaryKey"` // 主键,类型为 any (v2.27+)
|
|
|
|
|
|
> CreatedAt time.Time // 创建时间
|
|
|
|
|
|
> UpdatedAt time.Time // 更新时间
|
|
|
|
|
|
> DeletedAt gorm.DeletedAt // 软删除时间戳
|
|
|
|
|
|
> }
|
|
|
|
|
|
> ```
|
|
|
|
|
|
> 嵌入后即可拥有完整的审计追踪 + 软删除能力,无需每个 struct 重复声明。
|
|
|
|
|
|
|
|
|
|
|
|
> [!question] 思考
|
|
|
|
|
|
> 如果 `User` 同时嵌入了 `AuditRecord` 和 `gorm.Model`,而两者都有 `CreatedBy` 字段,会发生什么?
|
|
|
|
|
|
> **答**:编译报错——Go 不允许歧义字段访问。此时应只保留一个来源,或改为普通成员变量而非嵌入:`Audit AuditRecord`。
|
|
|
|
|
|
|
2026-04-28 20:02:42 +08:00
|
|
|
|
## 字段类型映射
|
|
|
|
|
|
|
|
|
|
|
|
| Go 类型 | 推荐 DB 类型 | 说明 |
|
|
|
|
|
|
|---------|-------------|------|
|
|
|
|
|
|
| `int`, `int64` | BIGINT | GORM 默认以 `int64` 处理 |
|
|
|
|
|
|
| `uint`, `uint64` | BIGINT UNSIGNED | — |
|
|
|
|
|
|
| `string` | VARCHAR(n) | 需手动指定 size |
|
|
|
|
|
|
| `bool` | TINYINT(1) / BOOLEAN | — |
|
|
|
|
|
|
| `float64` | DOUBLE | — |
|
|
|
|
|
|
| `time.Time` | DATETIME / TIMESTAMP | 配合 `autoTime` tag |
|
|
|
|
|
|
| `[]byte` | BLOB / BYTEA | 二进制数据 |
|
|
|
|
|
|
| `json.RawMessage` | JSON | JSON 序列化字段 |
|
|
|
|
|
|
|
|
|
|
|
|
### `autoTime` 自动时间戳
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type Article struct {
|
|
|
|
|
|
ID uint `gorm:"primaryKey"`
|
|
|
|
|
|
Title string `gorm:"size:128;not null"`
|
2026-04-28 20:12:15 +08:00
|
|
|
|
CreatedAt time.Time `gorm:"autoTime;createTime"` // 首次插入时自动填入当前时间,后续 UPDATE 不会修改
|
|
|
|
|
|
UpdatedAt time.Time `gorm:"autoTime;updateTime"` // 每次 UPDATE 时自动更新为当前时间
|
2026-04-28 20:02:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-28 20:12:15 +08:00
|
|
|
|
> [!tip] `autoTime` + `createTime` / `updateTime` 的分工
|
|
|
|
|
|
> - 只写 `autoTime`(旧写法):GORM 在 INSERT 和 UPDATE 时都更新时间字段。
|
|
|
|
|
|
> - `autoTime` + `createTime`:INSERT 时设置 `CreatedAt`,UPDATE 不碰它——这才是创建时间的正确语义。
|
|
|
|
|
|
> - `autoTime` + `updateTime`:UPDATE 时设置 `UpdatedAt`,INSERT 时同样会设初始值。
|
2026-04-28 20:02:42 +08:00
|
|
|
|
|
|
|
|
|
|
## 主键策略
|
|
|
|
|
|
|
|
|
|
|
|
### 默认策略:ID uint
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type User struct {
|
|
|
|
|
|
ID uint // GORM 默认将名为 ID 的字段视为主键
|
|
|
|
|
|
Name string
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### UUID 主键
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
import "github.com/google/uuid"
|
|
|
|
|
|
|
|
|
|
|
|
type Order struct {
|
|
|
|
|
|
ID uuid.UUID `gorm:"type:char(36);primaryKey"`
|
|
|
|
|
|
Code string `gorm:"uniqueIndex;not null"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 在 Create 前生成 UUID
|
|
|
|
|
|
order.ID = uuid.New()
|
|
|
|
|
|
db.Create(&order)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-28 20:12:15 +08:00
|
|
|
|
> [!tip] 为什么选 UUID 做主键?
|
|
|
|
|
|
> 自增 ID 存在 URL 泄露、ID 猜测等安全风险。UUID v4 完全随机,不会暴露数据量级。代价是索引碎片化更严重——InnoDB 的聚簇索引以主键排序,随机插入会导致页分裂,吞吐量下降约 30%。如果性能敏感可考虑 [ULID](https://github.com/ulid/spec)(有序且随机),或雪花算法生成的 `int64`。
|
|
|
|
|
|
|
2026-05-05 21:21:58 +08:00
|
|
|
|
> [!abstract] 子文档
|
|
|
|
|
|
> - [[02-模型定义/UUID主键策略]] — UUID 版本选型(v4/v7)、BeforeCreate 自动生成、存储优化(char(36) vs binary(16))、InnoDB 页分裂原理、ULID / Snowflake 替代方案、决策流程
|
|
|
|
|
|
|
2026-04-28 20:02:42 +08:00
|
|
|
|
### 复合主键
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type OrderItem struct {
|
2026-04-28 20:12:15 +08:00
|
|
|
|
OrderID uint `gorm:"primaryKey"` // 多个 primaryKey tag 构成复合主键
|
2026-04-28 20:02:42 +08:00
|
|
|
|
ProductID uint `gorm:"primaryKey"`
|
|
|
|
|
|
Quantity int `gorm:"not null"`
|
|
|
|
|
|
}
|
|
|
|
|
|
// 生成表结构:PRIMARY KEY (order_id, product_id)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-28 20:12:15 +08:00
|
|
|
|
> [!warning] 复合主键的注意事项
|
|
|
|
|
|
> - `First()` 和 `Take()` 无法工作(它们期望单主键),必须用 `Where("order_id=? AND product_id=?", ...)` 精确定位。
|
|
|
|
|
|
> - `AutoMigrate` 对复合主键支持有限,建议手动建表或在迁移后验证 schema。
|
|
|
|
|
|
> - 关联查询时外键也需要对应多列——通常此时改用单独的 `id` 作为主键 + 唯一索引代替复合主键更为方便。
|
|
|
|
|
|
|
|
|
|
|
|
## 软删除
|
|
|
|
|
|
|
|
|
|
|
|
GORM 的软删除通过 `DeletedAt` 字段实现——被"删除"的记录实际上只是将该字段设为当前时间,数据仍然保留在数据库中。
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type User struct {
|
|
|
|
|
|
gorm.Model // DeletedAt 已包含在此中
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// "删除"一条记录(实际是 UPDATE SET deleted_at = NOW())
|
|
|
|
|
|
db.Delete(&user) // sql: UPDATE users SET deleted_at=... WHERE id=?
|
|
|
|
|
|
|
|
|
|
|
|
// 查询时自动排除已删除记录
|
|
|
|
|
|
db.Find(&users) // WHERE deleted_at IS NULL
|
|
|
|
|
|
|
|
|
|
|
|
// 查询全部(包括已删除)
|
|
|
|
|
|
db.Unscoped().Find(&users)
|
|
|
|
|
|
|
|
|
|
|
|
// 真正物理删除
|
|
|
|
|
|
db.Unscoped().Delete(&user)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
> [!important] 软删除的设计哲学
|
|
|
|
|
|
> 软删除不是银弹。适合用于「可能需要恢复」的业务场景(如组织架构、订单流程)。但对于有唯一性约束的字段,软删除后会导致冲突——因为旧记录的该行仍占着唯一值。此时需自行设计隔离策略,例如添加 `is_deleted INT DEFAULT 0` 替代 GORM 内置方案。
|
|
|
|
|
|
|
|
|
|
|
|
> [!question] 为什么软删除用时间戳而非 BOOLEAN?
|
|
|
|
|
|
> 时间戳记录了确切的删除时刻,便于后续审计和问题排查;而 BOOLEAN 只能告诉你「是否删除」,丢失了时序信息。同时,`NULL` 表示未删除,语义更加清晰。
|
2026-04-28 20:02:42 +08:00
|
|
|
|
|
|
|
|
|
|
## 自定义类型作为字段
|
|
|
|
|
|
|
2026-05-06 12:28:28 +08:00
|
|
|
|
当内置类型不够用时,GORM 提供三组接口让 struct 自行控制与数据库的交互方式。详见子文档:
|
|
|
|
|
|
|
|
|
|
|
|
> [!abstract] 子文档
|
|
|
|
|
|
> - [[02-模型定义/自定义类型作为字段]] — ValueScanner 深度用法(JSON、枚举、IP 压缩、加密存储)、GORMDataType 类型覆盖、Resolver API、AutoMigrate 推导流程、常见坑点
|
2026-04-28 20:12:15 +08:00
|
|
|
|
|
|
|
|
|
|
### ValueScanner 模式(推荐)
|
|
|
|
|
|
|
|
|
|
|
|
实现 `driver.Valuer`(写回 DB)和 `sql.Scanner`(从 DB 读出)两个接口:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type JSONMap map[string]interface{}
|
|
|
|
|
|
|
|
|
|
|
|
// Value:Go → DB(序列化)
|
|
|
|
|
|
func (j JSONMap) Value() (driver.Value, error) {
|
|
|
|
|
|
if j == nil {
|
|
|
|
|
|
return nil, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return json.Marshal(j) // 存入时转为 JSON 字符串
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Scan:DB → Go(反序列化)
|
|
|
|
|
|
func (j *JSONMap) Scan(value interface{}) error {
|
|
|
|
|
|
if value == nil {
|
|
|
|
|
|
*j = nil
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return json.Unmarshal(value.([]byte), &j)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type Setting struct {
|
|
|
|
|
|
ID uint `gorm:"primaryKey"`
|
|
|
|
|
|
Key string `gorm:"size:64;uniqueIndex;not null"`
|
|
|
|
|
|
Value JSONMap `gorm:"type:json"` // 存入 MySQL JSON 列
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### GORMDataType 覆盖类型推导
|
|
|
|
|
|
|
|
|
|
|
|
如果你只需要告诉 GORM「这个类型对应什么数据库类型」,无需处理序列化逻辑:
|
2026-04-28 20:02:42 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-04-28 20:12:15 +08:00
|
|
|
|
type Priority int8 // 优先级:1-紧急 2-高 3-普通 4-低
|
2026-04-28 20:02:42 +08:00
|
|
|
|
|
2026-04-28 20:12:15 +08:00
|
|
|
|
func (Priority) GORMDataType() string {
|
|
|
|
|
|
return "TINYINT UNSIGNED" // 覆盖 GORM 默认的 INT 推导
|
2026-04-28 20:02:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 20:12:15 +08:00
|
|
|
|
type Task struct {
|
|
|
|
|
|
ID uint `gorm:"primaryKey"`
|
|
|
|
|
|
Title string `gorm:"size:128;not null"`
|
|
|
|
|
|
Priority Priority `gorm:"not null;default:3"`
|
2026-04-28 20:02:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-28 20:12:15 +08:00
|
|
|
|
> [!tip] 该用哪个接口?
|
|
|
|
|
|
> - 需要 **序列化成特定格式**(如 JSON、压缩字符串)→ 实现 `driver.Valuer` + `sql.Scanner`
|
|
|
|
|
|
> - 只需 **覆盖列类型**,不做特殊转换 → 实现 `GORMDataType()`
|
|
|
|
|
|
> - 两者可同时实现——`GORMDataType` 决定建表类型,`Value/Scan` 决定读写时的数据转换。
|
|
|
|
|
|
|
|
|
|
|
|
## 数据校验与生命周期钩子
|
|
|
|
|
|
|
|
|
|
|
|
struct tag 适合声明式约束,但更复杂的业务逻辑需要 GORM 提供的生命周期回调:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type User struct {
|
|
|
|
|
|
ID uint `gorm:"primaryKey"`
|
|
|
|
|
|
Email string `gorm:"size:128;uniqueIndex;not null"`
|
|
|
|
|
|
Password string `gorm:"size:128;not null"`
|
|
|
|
|
|
IsActive bool `gorm:"default:true"`
|
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BeforeCreate:在 INSERT 之前执行(可用于加密密码、生成唯一码等)
|
|
|
|
|
|
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
|
|
|
|
|
// 基础校验
|
|
|
|
|
|
if !strings.Contains(u.Email, "@") {
|
|
|
|
|
|
return errors.New("invalid email format") // 返回错误中断操作
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AfterFind:在 SELECT 之后执行(可用于解密敏感字段)
|
|
|
|
|
|
func (u *User) AfterFind(tx *gorm.DB) error {
|
|
|
|
|
|
// 例如:对查询结果做脱敏处理
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
> [!summary] 常用钩子一览
|
|
|
|
|
|
> | 钩子 | 触发时机 | 典型用途 |
|
|
|
|
|
|
> |------|----------|----------|
|
|
|
|
|
|
> | `BeforeCreate` | `Create` 之前 | 字段默认值、加密、校验 |
|
|
|
|
|
|
> | `AfterCreate` | `Create` 之后 | 发送通知、记录日志 |
|
|
|
|
|
|
> | `BeforeUpdate` | `Update` 之前 | 乐观锁版本检查 |
|
|
|
|
|
|
> | `AfterUpdate` | `Update` 之后 | 同步缓存、审计追踪 |
|
|
|
|
|
|
> | `BeforeDelete` | `Delete` 之前 | 关联级联清理 |
|
|
|
|
|
|
> | `AfterDelete` | `Delete` 之后 | 软删除相关辅助操作 |
|
|
|
|
|
|
> | `AfterFind` | `Find/First` 之后 | 字段解密、格式化输出 |
|
|
|
|
|
|
|
|
|
|
|
|
> [!note] 校验的最佳实践
|
|
|
|
|
|
> struct tag 的 `not null` / `uniqueIndex` 仅在 **建表** 时生效。运行时 GORM 不会自动校验这些数据——如果需要应用层校验,优先使用独立的 validator 库(如 `github.com/go-playground/validator`),而不是依赖生命周期钩子做业务校验。
|
|
|
|
|
|
|
2026-04-28 20:02:42 +08:00
|
|
|
|
## 模型设计流程图
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
2026-04-28 20:12:15 +08:00
|
|
|
|
graph TD
|
|
|
|
|
|
A[定义 struct] --> B{有无 ID<br/>字段?}
|
|
|
|
|
|
B -->|有| C[ID 自动作主键]
|
|
|
|
|
|
B -->|无| D{实现 TableName?}
|
|
|
|
|
|
D -->|已实现| E[使用返回值<br/>作为表名]
|
|
|
|
|
|
D -->|未实现| F[struct 名 →<br/>蛇形复数形式]
|
|
|
|
|
|
C --> G{需要自定义列名或<br/>类型?}
|
2026-04-28 20:02:42 +08:00
|
|
|
|
E --> G
|
2026-04-28 20:12:15 +08:00
|
|
|
|
F --> G
|
|
|
|
|
|
G -->|是| H[添加 gorm tag]
|
|
|
|
|
|
G -->|否| I{是否需要软删除?}
|
|
|
|
|
|
H --> I
|
|
|
|
|
|
I -->|是| J[嵌入 gorm.Model<br/>或使用 DeletedAt]
|
|
|
|
|
|
I -->|否| K{内置类型够用?}
|
|
|
|
|
|
J --> K
|
|
|
|
|
|
K -->|是| L[完成 ✓]
|
|
|
|
|
|
K -->|否| M[实现 Value / Scan<br/>接口]
|
|
|
|
|
|
M --> L
|
2026-04-28 20:02:42 +08:00
|
|
|
|
style A fill:#4FC08D,color:#fff
|
|
|
|
|
|
style C fill:#3B82F6,color:#fff
|
2026-04-28 20:12:15 +08:00
|
|
|
|
style J fill:#F59E0B,color:#fff
|
|
|
|
|
|
style L fill:#A0AEC0,color:#fff
|
2026-04-28 20:02:42 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 关联笔记
|
|
|
|
|
|
|
|
|
|
|
|
- [[01-安装与初始化]]
|
|
|
|
|
|
- [[03-CRUD 操作]]
|
|
|
|
|
|
- [[12-自定义字段类型]]
|