Skip to content

Instantly share code, notes, and snippets.

@weiland
Created September 29, 2016 12:58
Show Gist options
  • Save weiland/042a895b319a38ec2cb5a66430f401d5 to your computer and use it in GitHub Desktop.
Save weiland/042a895b319a38ec2cb5a66430f401d5 to your computer and use it in GitHub Desktop.
Find or Create in Ecto
# two different ways to implement a find_or_create for Ecto
find_or_create_user = fn user ->
case Repo.all(from u in users, where: u.id == ^user.id and u.email == ^user.email) do
[] ->
%User{}
|> User.changeset(user)
|> Repo.insert!()
_ ->
IO.puts "Already inserted"
end
end
def find_or_create(user) do
query = from u in users,
where: u.uid == ^user.uid
if !Repo.one(query) do
Repo.insert(user)
end
Repo.one(query)
end
@pragdave
Copy link

Do these have a race condition?

@maxim
Copy link

maxim commented Feb 15, 2018

Yeah, these are not safe implementations.

@protestContest
Copy link

This is what I would do:

def find_or_create(user_params) do
  {:ok, user} = %User{}
    |> User.changeset(user_params)
    |> Repo.insert(on_conflict: :nothing)

  if is_nil(user.id) do
    Repo.one from u in User, where: u.id == ^user.id
  else
    user
  end
end

With a unique constraint on the user table (probably something like email). This uses the on_conflict option to ensure the record exists before selecting, which should avoid race conditions: https://hexdocs.pm/ecto/Ecto.Repo.html#c:insert/2-upserts

@wyozi
Copy link

wyozi commented Oct 4, 2020

@protestContent this is better but still not the exactly correct behavior. At least on Postgres the serial autoincrement id increases even with on_conflict: :nothing, so with enough find_or_create calls this will needlessly increase autoincrement IDs by quite a bit.

@protestContest
Copy link

@wyozi good point, I didn't realize that. Still better than a race condition imo. I wonder what the postgres community recommends in cases like this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment