Ben Smith
Ben Smith

Reputation: 861

ecto cast_assoc find or create

When calling cast_assoc, I'd like a 'find or create' style behavior. Is there an easy way to do this in Ecto?

As a contrived example, a user belongs_to a favorite color, which is unique by color.name. If I create a new user with a favorite color that already exists, I get an error (name has already been taken). Instead I'd like to set user.color_id to the pre-existing color record. Is there a built in feature in Ecto to do this, or will I have to roll my own solution?

User changeset:

def changeset(struct, params \\ %{}) do
  struct
  |> cast(params, [:name])
  |> cast_assoc(:color)
end

Failing test:

test "create user with pre-existing color" do
  Repo.insert!(%Color{name: "red"})

  %User{}
  |> User.changeset(%{name: "Jim", color: %{name: "red"}})
  |> Repo.insert!
end

Upvotes: 5

Views: 1639

Answers (1)

oldhomemovie
oldhomemovie

Reputation: 15129

The way you put it, I'm afraid you would have to roll your own code: in the provided example, you're dealing with child assoc (user), attempting to manage it's parent (e.g. color). This has to be done manually.

The amount of the code to be added is really not that big though. Code for user creation would look like this:

color = Repo.get_by(Color, name: params["color"]["name"])

if color do
  %User{}
  |> User.changeset(params)
  |> Ecto.Changeset.put_assoc(:color, color)
else
  %User{}
  |> User.changeset(params)
  |> Ecto.Changeset.cast_assoc(:color)
end
|> Repo.insert!

Alternatively, you should reverse your approach - if you know color exists ahead of time (which I believe you should), you can do:

color = Repo.get_by(Color, name: params["color"]["name"])

color
|> build_assoc(:user)

This would of course require color to have has_one :user, User or has_many :users, User association declared in its schema.

Upvotes: 1

Related Questions