PAYATHON 2026

如何将 PostgreSQL schema 变更同步到 Elixir 模型并保留自定义代码?

数据小许

可以继续由 Node.js 统一管理数据库变更,Elixir 只维护数据库到模型的映射。要重复同步并保留自定义代码,需要把自动生成的结构定义与手写业务代码分开。

如果模型层使用 Ash,且 ash_postgres.gen.resources 支持 --fragments,可以使用该命令提供的工作流。如果只使用 Ecto,可以先生成到临时目录,审查差异后再更新模型。原问题没有提供依赖版本,无法确认当前项目是否支持这些 Ash 选项。

先区分数据库 schema、Ecto schema 和 Ash resource

数据库 schema 及其表结构由 Node.js 的迁移流程管理。Ecto schema 描述表字段与 Elixir struct 之间的映射,不会因为数据库发生变化而自动更新,也不要求数据库必须由 Ecto migrations 创建。参见 Ecto.Schema 文档。

Ash resource 包含属性、关系、actions 等定义。生成 Ecto schema 并不等于生成了可直接使用的 Ash resource,因此要根据业务层使用的模型选择生成流程。

使用 Ash 时,通过 fragments 保留手写逻辑

先检查项目安装版本提供的选项:

mix help ash_postgres.gen.resources

如果支持 --fragments 和 --no-migrations,可以按以下方式从 public schema 生成资源:

mix ash_postgres.gen.resources RealtimeService.Db.Public \
  --repo RealtimeService.Repo.Public \
  --tables public. \
  --no-migrations \
  --fragments

RealtimeService.Db.Public 是示例 Ash domain 名称,应按项目实际结构调整。--tables public. 表示该 schema 下的所有表,也可以使用 public.messages 这样的限定表名。参数规则见 资源生成命令文档。

生成后的代码分为两部分:

  • resource 文件保存手写的 actions、validations、calculations 等逻辑。
  • fragment 文件保存从数据库读取的 attributes、relationships 和 identities。

数据库变更完成后,再执行相同命令。该工作流会重新生成 fragment,同时保留已存在的 resource 文件。--no-migrations 会关闭本次资源生成的 snapshots 和 migrations,并在生成的资源中设置 migrate? false。具体行为见 AshPostgres 现有数据库工作流。

不要把需要长期保留的修改直接写进 fragment。已有的手写模型也需要先整理成这种结构,不能假定首次执行命令就会自动合并所有自定义内容。

对于 elx schema,可以使用相同流程,将表范围改为 elx.,并选择有相应访问权限的 Repo。即使 Elixir 可以写入 elx 的业务数据,数据库结构变更仍可由 Node.js 统一管理。

enum 和 jsonb 如何映射

以下示例针对普通 Ecto schema,不应直接粘贴到 Ash resource 中。假设示例表为 public.messages,主键是整数,status 的数据库枚举标签为 pending、sent,payload 保存 JSON 对象:

defmodule RealtimeService.Db.Public.Models.Message do
  use Ecto.Schema

  @schema_prefix "public"
  @primary_key false

  schema "messages" do
    field :id, :integer, primary_key: true
    field :status, Ecto.Enum, values: [:pending, :sent]
    field :payload, :map
  end
end

这些表名、字段名和枚举值仅用于说明,需要替换为实际定义。

PostgreSQL enum

Ecto.Enum 可以把数据库中的字符串标签映射为 Elixir atom,也可与 PostgreSQL 原生 enum 配合使用。枚举值必须明确列出;数据库返回未包含在映射中的值时,加载会失败。参见 Ecto.Enum 文档。

enum 变更需要考虑部署顺序:新增标签后,应在开始写入新值之前更新会读取该字段的 Elixir 服务。如果生成器没有处理 enum,需要补充其类型映射或模板,不能只依赖首次生成后的手工修补。

jsonb

如果字段约定始终保存 JSON 对象,可以使用:

field :payload, :map

Ecto 的 :map 可以映射 PostgreSQL 的 jsonb。数据库中的 JSON 对象通常会加载为带字符串键的 map。参见 Ecto 的 map 类型说明。

jsonb 也能保存数组和标量,列类型本身不能说明内容一定是对象。只有实际数据符合这一约定时,才适合统一映射为 :map。PostgreSQL array 与 jsonb 中的 JSON 数组也应分别处理。

使用 Ash 时,需要检查生成的 Ash 属性类型及 enum 定义;上述 Ecto 声明不会自动修复 Ash resource 的类型映射。

继续使用 introspex 时,怎样重复同步

目前已知,现有使用方式生成了初始文件,部分字段变成了注释,后续更新没有达到预期。由于缺少 introspex 版本和第二次执行时的输出,无法判断具体原因,也不能据此断言所有版本都不支持更新。

可以先采用以下流程:

  1. 由 Node.js migrations 更新开发或临时数据库。
  2. 将重新生成的模型输出到独立目录,避免覆盖现有源码。
  3. 比较新增字段、删除字段、类型变化、主键、enum 和关系定义。
  4. 将确认后的差异应用到项目模型,再编译并验证相关查询。

如果需要进一步自动化,应让生成器只负责可替换的结构代码,把业务函数和 changeset 逻辑放到单独模块。对于自动生成后仍需修正的 enum、jsonb 映射,应把规则加入生成模板或生成后的处理流程。

数据库结构差异不能完整表达业务意图。例如,仅凭结构快照,无法可靠判断删除一列并新增另一列是否属于字段重命名。即使实现自动生成,也应保留差异审查。

public 只读需要单独落实

@schema_prefix "public" 负责定位数据库 schema,不会赋予只读属性;Repo 名称包含 Public 也不会限制写入。Ash 的 migrate? false 只涉及迁移生成,同样不限制业务数据写入。

对 public 的只读要求应由 PostgreSQL 角色权限落实,并在 Ash resource 中只保留需要的读取 actions。生成后检查默认 actions,避免暴露不需要的写入操作。可以为 elx 授予所需的数据读写权限,DDL 继续由 Node.js 的迁移身份执行。

备注:内容仅供参考。