使用 Ecto (MySQL) 进行 upsert 的最简单方法是什么

What is the simplest way to do upsert with Ecto (MySQL)

upsert 在我的应用程序中很常见,我想采用最干净、最简单的方式来实现 upsert。

  1. 我应该使用片段来实现原生 sql upsert 吗?
  2. 有什么惯用的 ecto 方式来做 upsert 吗?

您可以使用 Ecto.Repo.insert_or_update/2,请注意,要使其正常工作,您必须从数据库中加载现有模型。

 model = %Post{id: 'existing_id', ...}
 MyRepo.insert_or_update changeset
 # => {:error, "id already exists"}

示例:

result =
  case MyRepo.get(Post, id) do
    nil  -> %Post{id: id} # Post not found, we build one
    post -> post          # Post exists, using it
  end
  |> Post.changeset(changes)
  |> MyRepo.insert_or_update

case result do
  {:ok, model}        -> # Inserted or updated with success
  {:error, changeset} -> # Something went wrong
end

如果您希望通过 id 以外的内容进行更新,您可以将 get_by 换成 get,如下所示:

model = %User{email: "existing_or_new_email@heisenberg.net", name: "Cat", ...}

model |> User.upsert_by(:email)
# => {:found, %User{...}} || {:ok, %User{...}}

defmodule App.User do
  alias App.{Repo, User}

  def upsert_by(%User{} = record_struct, selector) do
    case User |> Repo.get_by({selector, record_struct |> Map.get(selector)}) do
      nil -> %User{} # build new user struct
      user -> user   # pass through existing user struct
    end
    |> User.changeset(record_struct |> Map.from_struct)
    |> Repo.insert_or_update
  end
end

如果您正在寻找一种适用于跨模型和多个选择器(即国家/地区 + 护照号码)的灵活方法,请查看我的十六进制包 EctoConditionals

在我的例子中 insert_or_update 由于唯一索引约束

引发了错误

对我有用的是 Postgres v9.5 通过 on_conflict 参数更新插入:

(考虑到唯一列称为 user_id

changeset
|> MyRepo.insert(
    on_conflict: :replace_all,
    conflict_target: :user_id
)