使用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的显式关联
如果需要保留连接表的时间戳,或者后续可能给连接表添加其他字段,推荐这种方法:
- 创建
RolesPermissionSchema文件(路径示例: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
- 修改
RoleSchema的关联定义,替换原有的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
- 更新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
相关产品推荐
相关产品推荐

