Ecto插入冲突时无法重置deleted_at为nil的技术求助
deleted_at为nil 我明白你的需求:当插入用户时遇到邮箱冲突(因为邮箱是唯一约束),希望把现有记录的deleted_at字段强制重置为nil,不管它之前的值是什么,而且这个字段可能没在你的changeset里传递或被允许。
问题根源
你之前用on_conflict: :replace_all的方式,只会更新changeset中包含的字段。如果deleted_at不在changeset的字段列表里(比如你的changeset没有允许这个字段,或者你没在attrs里传递它),Ecto就不会去更新这个字段。所以我们需要明确指定要更新的字段,强制把deleted_at设为nil。
方法1:自定义on_conflict的set语句
这种方式最直接,明确列出要更新的字段,同时强制设置deleted_at: nil:
# 不需要在attrs里传deleted_at,我们直接在on_conflict里设置 attrs = %{"email" => "tanweer.shahzaad@gmail.com"} cs = %User{} |> User.changeset(attrs) ExApi.Repo.insert(cs, on_conflict: [ set: [ # 保留新生成的邮箱确认令牌(你的schema默认会生成这个值) email_confirmation_token: excluded.email_confirmation_token, # 强制重置deleted_at为nil deleted_at: nil, # 手动更新updated_at,或依赖Ecto自动处理(取决于你的timestamps配置) updated_at: DateTime.utc_now() ] ], conflict_target: :email )
这里的excluded是Ecto提供的特殊变量,代表原本要插入但冲突了的那条记录的字段值。用它来获取新生成的email_confirmation_token,就能把新的令牌更新到现有记录上。
方法2:结合replace_all_except和额外设置
如果你希望保留changeset里所有字段的更新,同时只强制重置deleted_at,可以用replace_all_except排除deleted_at,再用set覆盖它:
attrs = %{"email" => "tanweer.shahzaad@gmail.com"} cs = %User{} |> User.changeset(attrs) ExApi.Repo.insert(cs, on_conflict: [ # 替换changeset里除了deleted_at之外的所有字段 :replace_all_except, [:deleted_at], # 强制把deleted_at设为nil set: [deleted_at: nil] ], conflict_target: :email )
额外注意:确保Changeset允许必要字段
如果你的User.changeset没有允许email_confirmation_token等字段,那即使你在on_conflict里用excluded,这些字段也不会被正确传递。所以你的User.changeset应该包含这些字段的cast操作:
def changeset(user, attrs) do user |> cast(attrs, [:email, :email_confirmation_token, :email_confirmed_at, :deleted_at]) |> validate_required([:email]) # 其他验证逻辑... end
如果不想让外部修改deleted_at,你可以在changeset里添加条件验证,或者直接在on_conflict里强制设置——因为on_conflict的set是直接操作数据库的,不受changeset字段权限限制。
内容的提问来源于stack exchange,提问作者Radio Active

