You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用ExMachina工厂创建Elixir关联表时遇inserted_at非空约束错误

问题:多对多关联表timestamps字段插入null触发数据库约束错误

背景

我正在完成一项测试,要求仅当用户拥有标签为Prescription Approval Role且包含approve_scripts权限的角色时,才能创建处方。为完成测试,我使用Elixir的ex_machina工具配置了以下工厂:

def user_factory do
  %User{
    id: Ecto.UUID.generate(),
    emr_id: Ecto.UUID.generate(),
    username: sequence(:user_username, "user-#{&1})"),
    name_first: sequence(:user_name_first, "user-#{&1})"),
    name_middle: sequence(:user_name_middle, "user-#{&1})"),
    name_last: sequence(:user_name_last, "user-#{&1})"),
    roles: [build(:role)],
    type: "practice"
  }
end

def permission_factory do
  %Permission{
    key: "approve_scripts",
    label: "Prescription Approval Permission",
    description: "Permission to approve scripts"
  }
end

def role_factory do
  %Role{
    label: "Prescription Approval Role",
    description: "Role needed to approve prescriptions",
    permissions: [build(:permission)]
  }
end

对应的数据库Schema定义如下:

schema "users" do
  field :emr_id, :string
  field :username, :string
  field :name_first, :string
  field :name_middle, :string
  field :name_last, :string
  field :type, Ecto.Enum, values: @user_types

  many_to_many :roles, Role, join_through: "users_roles", on_replace: :delete

  has_many :permissions, through: [:roles, :permissions]

  timestamps()
end

schema "roles" do
  field :label, :string
  field :description, :string

  many_to_many(:permissions, Permission,
    join_through: "roles_permissions",
    join_keys: [role_id: :id, permission_key: :key],
    on_replace: :delete
  )

  many_to_many(:users, User, join_through: "users_roles")

  timestamps()
end
 
schema "permissions" do
  field :label, :string
  field :description, :string

  timestamps()
end

运行任何使用insert(:user)的测试时,都会收到以下错误:

** (Postgrex.Error) ERROR 23502 (not_null_violation) null value in column "inserted_at" of relation "roles_permissions" violates not-null constraint

     table: roles_permissions
     column: inserted_at

 Failing row contains (a63ceb35-3e7f-428d-8328-05e05a3fe6bb, approve_scripts, null, null).

我清楚问题所在:底层的roles_permissions关联表需要inserted_at和updated_at字段,但不知道如何修复。原以为这些字段会自动插入,不理解为何会出现null值。是否需要创建roles_permissions对应的Schema文件?恳请提供帮助与指导。

编辑补充:当前的Elixir数据库迁移代码如下:

create table(:users) do
  add :emr_id, :citext
  add :username, :citext, null: false
  add :name_first, :text, null: false
  add :name_middle, :text
  add :name_last, :text, null: false
  add :type, :user_type, null: false
  timestamps()
end

create table("roles") do
  add :label, :text, null: false
  add :description, :text, null: false
  timestamps()
end

create table("permissions", primary_key: false) do
  add :key, :text, primary_key: true
  add :label, :text, null: false
  add :description, :text, null: false
  timestamps()
end

create table("roles_permissions", primary_key: false) do
  add :role_id, references(:roles), primary_key: true
  add :permission_key, references(:permissions, column: :key, type: :text), primary_key: true
  timestamps()
end

create table("users_roles", primary_key: false) do
  add :user_id, references(:users), primary_key: true
  add :role_id, references(:roles), primary_key: true
  timestamps()
end

解决方案

Ecto默认不会为many_to_many的纯连接表自动填充timestamps字段,因为它把这类表视为仅存储关联关系的中间表,不会自动注入时间戳值。针对这个问题,有两种可行的解决思路:

方法1:将连接表改为带Schema的显式关联

如果需要保留连接表的时间戳,或者后续可能给连接表添加其他字段,推荐这种方法:

  1. 创建RolesPermission Schema文件(路径示例:lib/your_app/roles_permission.ex):
defmodule YourApp.RolesPermission do
  use Ecto.Schema
  import Ecto.Changeset

  schema "roles_permissions" do
    belongs_to :role, YourApp.Role
    belongs_to :permission, YourApp.Permission, references: :key, foreign_key: :permission_key

    timestamps()
  end

  def changeset(roles_permission, attrs) do
    roles_permission
    |> cast(attrs, [:role_id, :permission_key])
    |> validate_required([:role_id, :permission_key])
  end
end
  1. 修改Role Schema的关联定义,替换原有的many_to_many为显式的has_many关联:
schema "roles" do
  field :label, :string
  field :description, :string

  # 替换原来的many_to_many :permissions
  has_many :roles_permissions, YourApp.RolesPermission, on_replace: :delete
  has_many :permissions, through: [:roles_permissions, :permission]

  many_to_many(:users, YourApp.User, join_through: "users_roles")

  timestamps()
end
  1. 更新ex_machina工厂,使用显式的连接表记录创建关联:
def role_factory do
  %Role{
    label: "Prescription Approval Role",
    description: "Role needed to approve prescriptions",
    roles_permissions: [build(:roles_permission)]
  }
end

def roles_permission_factory do
  %RolesPermission{
    role: build(:role),
    permission: build(:permission)
  }
end

方法2:移除连接表的timestamps字段

如果不需要追踪连接关系的创建/更新时间,可以直接修改迁移文件,去掉连接表的timestamps()定义:

create table("roles_permissions", primary_key: false) do
  add :role_id, references(:roles), primary_key: true
  add :permission_key, references(:permissions, column: :key, type: :text), primary_key: true
  # 移除timestamps()
end

create table("users_roles", primary_key: false) do
  add :user_id, references(:users), primary_key: true
  add :role_id, references(:roles), primary_key: true
  # 移除timestamps()
end

修改后,在测试环境重置数据库并重新运行迁移即可生效。

额外说明

  • 方法1的优势是灵活性更高,后续可以给连接表添加备注、状态等自定义字段;
  • 方法2更简洁,适合仅需存储关联关系的场景;
  • 原代码中使用build/1创建关联,insert/1时Ecto会自动处理关联插入,但由于连接表没有对应的Schema,Ecto无法自动填充timestamps字段,因此导致null值违反数据库约束。

内容的提问来源于stack exchange,提问作者Dan Rubio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 13:32:35