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

9.0 KiB
Raw Blame History

新增 model 流程

项目 models 定义于 backend/database/models 文件中。 每个 model 对应数据库中的一张表。 新增 model 的步骤如下:

步骤

  1. 运行 atomctl migrate create [alter|create_table] 创建迁移文件。
  2. 编辑生成的迁移文件,定义数据库表结构变更。不需要声明 BEGINCOMMIT框架会自动处理。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 类型。例如:
// swagger:enum UserStatus
// ENUM(pending_verify, verified, banned, )
type UserStatus string
  1. 执行 atomctl gen enum,生成 pkg/consts/[table].gen.go

其它支持的数据类型

database/.transform.yamlfield_type 支持将表字段映射为 go.ipao.vip/gen/types 提供的 PostgreSQL 扩展类型(在 .transform.yamlimports 中引入 go.ipao.vip/gen 后,通常可直接使用 types.*)。

常用类型清单(对应 gen/types/

  • types.JSONjson/jsonb(建议列类型用 jsonb
  • types.JSONMapjson/jsonbmap[string]any 形态
  • types.JSONType[T] / types.JSONSlice[T]:强类型 JSON读写用不提供 JSON 路径查询能力)
  • types.Array[T]PostgreSQL 数组(如 text[]/int[] 等)
  • types.UUID / types.BinUUIDuuidBinUUID 主要用于二进制存储场景)
  • types.Date / types.Timedate / time
  • types.Moneymoney
  • types.URLURL通常落库为 text/varchar,由类型负责解析/序列化)
  • types.XMLxml
  • types.HexBytesbyteahex 表示)
  • types.BitStringbit/varbit
  • 网络类型:types.Inetinet)、types.CIDRcidr)、types.MACAddrmacaddr
  • 范围类型:
    • types.Int4Rangeint4range
    • types.Int8Rangeint8range
    • types.NumRangenumrange
    • types.TsRangetsrange
    • types.TstzRangetstzrange
    • types.DateRangedaterange
  • 几何类型:types.Point / types.Polygon / types.Box / types.Circle / types.Path
  • 全文检索:types.TSQuery / types.TSVector
  • 可空类型:types.Null[T] 以及别名 types.NullString/NullInt64/...(需要字段允许 NULL

示例(database/.transform.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 关系标签。 示例:

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_tohas_onehas_manymany_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_idteacher_id)转换为结构体字段名(如 ClassIDTeacherID),并据此写入正确的 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

    • YAMLusers 下配置 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

    • YAMLstudents 下配置 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 的使用。例如:

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 层调用,不需要与表进行一一对应。 示例:

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 serviceatomctl gen provider 完成依赖对象注入。 service 调用 model 示例:

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
}
···