Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
like image 906
Ben Smith Avatar asked Nov 09 '22 06:11

Ben Smith


1 Answers

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.

like image 141
gmile Avatar answered Nov 15 '22 09:11

gmile