Phoenix中Ecto upsert更新触发主键约束错误,on_conflict为何失效?
Phoenix Tag 实体 Upsert 问题排查与解决
问题核心分析
你遇到的tag_pkey主键冲突,本质是upsert_tag函数在更新现有Tag时,拿着带主键的结构体执行插入逻辑,而非基于etiqueta的唯一索引触发冲突处理。Ecto的on_conflict只会响应你指定的冲突约束,若未明确绑定到etiqueta的唯一索引,数据库会优先检测主键冲突。
分步修正与代码示例
1. 确认迁移中的唯一索引配置
先确保etiqueta的唯一索引创建正确:
def change do create table(:tags) do add :etiqueta, :string, null: false # 其他字段 timestamps() end # 明确创建etiqueta字段的唯一索引,可自定义名称 create unique_index(:tags, [:etiqueta], name: :tags_etiqueta_unique_index) end
2. 保持Schema与Changeset的正确性
确保Changeset允许修改etiqueta并绑定唯一约束:
defmodule MyApp.Tag do use Ecto.Schema import Ecto.Changeset schema "tags" do field :etiqueta, :string # 其他字段 timestamps() end def changeset(tag, attrs) do tag |> cast(attrs, [:etiqueta]) # 加入所有需要更新的字段 |> validate_required([:etiqueta]) |> unique_constraint(:etiqueta, name: :tags_etiqueta_unique_index) end end
3. 修复Upsert函数逻辑
核心是基于etiqueta字段触发冲突检测,而非依赖主键:
def upsert_tag(nil, attrs) do # 无现有结构体时,直接基于属性执行upsert %Tag{} |> Tag.changeset(attrs) |> Repo.insert( on_conflict: {:replace, [:etiqueta, :updated_at]}, # 列出需要更新的字段 conflict_target: :etiqueta, # 指定触发冲突的唯一字段 returning: true ) end def upsert_tag(%Tag{} = existing_tag, attrs) do # 有现有结构体时,合并属性后依然基于etiqueta执行upsert existing_tag |> Tag.changeset(attrs) |> Repo.insert( on_conflict: {:replace, [:etiqueta, :updated_at]}, conflict_target: :tags_etiqueta_unique_index, # 也可直接用索引名称 returning: true ) end
关键注意事项
- 避免携带主键的结构体直接执行
Repo.insert,这会让数据库优先检测主键冲突,而非你期望的etiqueta唯一索引冲突。 on_conflict的:replace列表要包含所有需要更新的字段,若要更新所有可修改字段,可改用:update_all。conflict_target必须准确指向etiqueta的唯一索引(字段名或索引名称均可)。
内容的提问来源于stack exchange,提问作者Zoey
相关产品推荐
相关产品推荐

