Ecto 条件更新插入 - Ecto.StaleEntryError

问题描述

我正在尝试完成智能 upsert,我将其定义为:

  • 如果数据库中没有具有相同 id 的模型,则执行 INSERT
  • 如果 db 中存在具有相同 ID 的条目并且该条目较新(updated_at 字段)请勿更新
  • 如果 db 中存在具有相同 id 的条目并且该条目较旧(updated_at 字段),请执行更新

我看到 repo.insert 可以选择将查询作为 :on_conflict 选项传递。 我决定编写满足我需求的简约源代码

update_query =
  from s in User,where: s.id == ^id,where: s.updated_at < ^updated_at,update: ^[set: Map.to_list(data)]

%User{}
|> Ecto.Changeset.change(data)
|> @repo.insert(conflict_target: :id,on_conflict: update_query)

代码仅在插入发生时有效。发生冲突时会导致 (Ecto.StaleEntryError) attempted to insert a stale struct: 错误。我可以从 update_query 中删除 where: s.id == ^id,where: s.updated_at < ^updated_at删除错误,但是随后我丢失了所需的 updated_at 检查。 所需的 postgres 代码看起来像

INSERT INTO mytable (id,entry) VALUES (42,'2021-05-29 12:00:00')
ON CONFLICT (id)
   DO UPDATE SET entry = EXCLUDED.entry,......
      WHERE mytable.entry < EXCLUDED.entry;

解决方法

我认为执行以下操作可能更容易:

def smart_upsert(%{id: id,updated_at: updated_at} = data) do
    query =
      from u in User,where: u.id == ^id,where: u.updated_at < ^updated_at
     
    User
    |> Repo.get_by(query)
    |> case do
      nil ->
        %User{}

      existing_user ->
        existing_user
    end
    |> User.changeset(data)
    |> Repo.insert_or_update()
  end

诚然,这需要 2 次数据库操作。