# 新增 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
}
···
```
