Files
quyun-v2/backend/docs/dev/model.md
2025-12-17 17:50:14 +08:00

227 lines
9.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 新增 model 流程
项目 `models` 定义于 `backend/database/models` 文件中。 每个 `model` 对应数据库中的一张表。 新增 `model` 的步骤如下:
## 步骤
1. 运行 `atomctl migrate create [alter|create_table]` 创建迁移文件。
2. 编辑生成的迁移文件,定义数据库表结构变更。不需要声明 `BEGIN``COMMIT`框架会自动处理。table 名称使用复数形式,例如 `tenants`
3. 执行 `atomctl migrate up` 应用迁移,更新数据库结构。
4. 对于 `JSON` `ARRAY` `ENUM` 等复杂字段类型,编辑 `database/.transform.yaml` 文件,在 `field_type.[table_name]` 定义字段与 Go 类型的映射关系。支持定义的数据类型参考数据类型章节
5. 运行 `atomctl gen model` 生成或更新 `models` 代码,确保代码与数据库结构同步。
## 数据类型
### Enum 类型
不使用数据库原生 `ENUM` 类型,使用业务代码来声明枚举类型,步骤如下:
1.`pkg/consts/[table].go` 文件中,定义枚举字段的 Go 类型。例如:
```go
// swagger:enum UserStatus
// ENUM(pending_verify, verified, banned, )
type UserStatus string
```
2. 执行 `atomctl gen enum`,生成 `pkg/consts/[table].gen.go`
### 其它支持的数据类型
`database/.transform.yaml``field_type` 支持将表字段映射为 `go.ipao.vip/gen/types` 提供的 PostgreSQL 扩展类型(在 `.transform.yaml``imports` 中引入 `go.ipao.vip/gen` 后,通常可直接使用 `types.*`)。
常用类型清单(对应 `gen/types/`
- `types.JSON``json/jsonb`(建议列类型用 `jsonb`
- `types.JSONMap``json/jsonb``map[string]any` 形态
- `types.JSONType[T]` / `types.JSONSlice[T]`:强类型 JSON读写用不提供 JSON 路径查询能力)
- `types.Array[T]`PostgreSQL 数组(如 `text[]/int[]` 等)
- `types.UUID` / `types.BinUUID``uuid``BinUUID` 主要用于二进制存储场景)
- `types.Date` / `types.Time``date` / `time`
- `types.Money``money`
- `types.URL`URL通常落库为 `text/varchar`,由类型负责解析/序列化)
- `types.XML``xml`
- `types.HexBytes``bytea`hex 表示)
- `types.BitString``bit/varbit`
- 网络类型:`types.Inet``inet`)、`types.CIDR``cidr`)、`types.MACAddr``macaddr`
- 范围类型:
- `types.Int4Range``int4range`
- `types.Int8Range``int8range`
- `types.NumRange``numrange`
- `types.TsRange``tsrange`
- `types.TstzRange``tstzrange`
- `types.DateRange``daterange`
- 几何类型:`types.Point` / `types.Polygon` / `types.Box` / `types.Circle` / `types.Path`
- 全文检索:`types.TSQuery` / `types.TSVector`
- 可空类型:`types.Null[T]` 以及别名 `types.NullString/NullInt64/...`(需要字段允许 NULL
示例(`database/.transform.yaml`
```yaml
imports:
- go.ipao.vip/gen
- quyun/v2/pkg/consts
field_type:
users:
roles: types.Array[consts.Role]
meta: types.JSON
home_ip: types.Inet
profile: types.JSONType[Profile]
tenants:
uuid: types.UUID
```
### 关联关系字段说明(对齐 GORM
支持在 `database/.transform.yaml` 中为模型定义关联关系字段,生成对应的 GORM 关系标签。
示例:
```yaml
field_relate:
students:
Class:
# belong_to, has_one, has_many, many_to_many
relation: belongs_to
table: classes
references: id # 关联表的主键/被引用键(通常是 id
foreign_key: class_id # 当前表上的外键列(如 students.class_id
json: class
Teachers:
# belong_to, has_one, has_many, many_to_many
relation: many_to_many
table: teachers
pivot: class_teacher
foreign_key: class_id # 当前表students用于关联的键转为结构体字段名 ClassID
join_foreign_key: class_id # 中间表中指向当前表的列class_teacher.class_id
references: id # 关联表teachers被引用的列转为结构体字段名 ID
join_references: teacher_id # 中间表中指向关联表的列class_teacher.teacher_id
json: teachers
teachers:
Classes:
relation: many_to_many
table: classes
pivot: class_teacher
classes:
Teachers:
relation: many_to_many
table: teachers
pivot: class_teacher
```
关联关系配置项如下:
- relation
- 取值:`belongs_to``has_one``has_many``many_to_many`
- 对应 GORM 的四种关系Belongs To、Has One、Has Many、Many2Many。
- table
- 关联的目标表名(即另一侧模型对应的表)。
- pivot仅 many_to_many
- 多对多中间表名称,对应 GORM 标签 `many2many:<pivot>`
- foreign_key按关系含义不同
- 对应 GORM 标签 `foreignKey:<Field>`
- belongs_to当前表上的外键列例如 `students.class_id`),会映射为当前模型上的字段(如 `ClassID`)。
- has_one / has_many外键在对端表上例如 `credit_cards.user_id`)。配置时仍在当前表的配置块里填“外键列名”,生成时会正确落到 GORM 标签中。
- references按关系含义不同
- 对应 GORM 标签 `references:<Field>`
- belongs_to对端表被引用的列一般是 `id`),映射为对端模型字段名(如 `ID`)。
- has_one / has_many被对端外键引用的当前模型列一般是当前模型的 `ID` 字段)。
- join_foreign_key仅 many_to_many
- 对应 GORM 标签 `joinForeignKey:<Field>`,指中间表里“指向当前模型”的列(如 `class_teacher.class_id`)。
- join_references仅 many_to_many
- 对应 GORM 标签 `joinReferences:<Field>`,指中间表里“指向关联模型”的列(如 `class_teacher.teacher_id`)。
说明:生成器会结合数据库的 NamingStrategy 将列名(如 `class_id``teacher_id`)转换为结构体字段名(如 `ClassID``TeacherID`),并据此写入正确的 GORM 标签。
### 与 GORM 标签的对应关系
- belongs_to 示例students → classes
- YAML
- `foreign_key: class_id`
- `references: id`
- 生成的模型字段(示意):
- `Class Class gorm:"foreignKey:ClassID;references:ID"`
- has_many 示例users → credit_cards
- YAML`users` 下配置 `CreditCards` 关系):
- `relation: has_many`
- `table: credit_cards`
- `foreign_key: user_id` (对端表上的外键列)
- `references: id` (当前模型被引用的列)
- 生成的模型字段(示意):
- `CreditCards []CreditCard gorm:"foreignKey:UserID;references:ID"`
- many_to_many 示例students ⇄ teachers经由 class_teacher
- YAML`students` 下配置 `Teachers` 关系):
- `relation: many_to_many`
- `table: teachers`
- `pivot: class_teacher`
- `foreign_key: class_id`
- `join_foreign_key: class_id`
- `references: id`
- `join_references: teacher_id`
- 生成的模型字段(示意):
- `Teachers []Teacher gorm:"many2many:class_teacher;foreignKey:ClassID;references:ID;joinForeignKey:ClassID;joinReferences:TeacherID"`
提示GORM 在 many2many 下允许省略部分键,生成器也支持“只给必要字段”。若不确定,建议显式全部写出,避免命名不一致导致推断失败。
## model 功能扩展
模型生成完后可以创建 `database/models/[table].go` 文件添加自定义方法、GORM Hook 来扩展 model 的使用。例如:
```go
func (m *User) ComparePassword(ctx context.Context, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(m.Password), []byte(password))
return err == nil
}
```
## 生成模型的使用
模型通常在 service 中使用service 定义于 `app/services`, 通常用于对一类功能进行封装,方便 controller 层调用,不需要与表进行一一对应。 示例:
```go
package services
import "context"
// @provider
type test struct{
app *app.Config
}
func (t *test) Test(ctx context.Context) (string, error) {
return "Test", nil
}
```
struct 中可以定义一个多上需要注入的 provider 对象,示例中的 app 会自动注入对应的 app.Config 实例。
service 文件创建完成后需要运行 `atomctl gen service``atomctl gen provider` 完成依赖对象注入。
service 调用 model 示例:
```go
func (t *test) FindByID(ctx context.Context, userID int64) (*models.User, error) {
tbl, query := models.UserQuery.QueryContext(ctx)
model, err := query.Preload(tbl.OwnedTenant, tbl.Tenants).Where(tbl.ID.Eq(userID)).First()
if err != nil {
return nil, errors.Wrapf(err, "FindByID failed, %d", userID)
}
return model, nil
}
···
```