Misunderstanding `put_assoc` for updating associated records?

2,625 views
Skip to first unread message

ev...@sherwood.io

unread,
Mar 2, 2016, 11:18:08 PM3/2/16
to elixir-ecto
Hi all,

Been trying to get this to work (or any permutation thereof):

from(d in Dish, where: d.id == 20, preload: [:vendor, :dietary_prefs]) |> Repo.first |> Dish.changeset(%{}) |> Ecto.Changeset.put_assoc(:dietary_prefs, dietary_prefs)

Where

dietary_prefs = from(dp in Mp.DietaryPref, where: dp.id in ^[1,2]) |> Repo.all

and

defmodule Mp.Dish do
 
use Mp.Web, :model

  schema
"dishes" do
    belongs_to
:vendor, Mp.Vendor
    many_to_many
:dietary_prefs, Mp.DietaryPref,
      join_through
: Mp.DishDietaryPref, on_delete: :delete_all,
      on_replace
: :delete
 
end

    # ...

 
def changeset(model, params \\ :invalid) do
    model
   
|> cast(params, @required_fields ++ @optional_fields)
   
|> cast_assoc(:dietary_prefs)
   
|> cast_assoc(:vendor)
   
|> validate_required(@required_fields)
 
end
end

and

defmodule Mp.DietaryPref do
 
use Mp.Web, :model

  schema
"dietary_prefs" do
    many_to_many
:dishes, Mp.Dish,
      join_through
: Mp.DishDietaryPref, on_delete: :delete_all,
      on_replace
: :delete
 
end

 
# ...

 
def changeset(model, params \\ :invalid) do
    model
   
|> cast(params, @required_fields ++ @optional_fields)
   
|> cast_assoc(:dishes)
   
|> validate_required(@required_fields)
 
end
end

I keep getting

** (RuntimeError) cannot change `dietary_prefs` with a struct because another embed/association is set in parent struct, use a changeset instead
    (ecto) lib/ecto/changeset/relation.ex:150: Ecto.Changeset.Relation.do_change/4
    (ecto) lib/ecto/changeset/relation.ex:264: Ecto.Changeset.Relation.map_changes/7
    (ecto) lib/ecto/changeset.ex:865: Ecto.Changeset.put_relation/5

I know I'm missing some subtlety about how Changesets with associations work, but every permutation I've tried hasn't yielded any results, and I've been unsuccessful finding an example of this online (found examples of creating new records—which I have working—but not updating existing records... particularly with how `on_replace` works).

Would really appreciate some help understanding what I'm doing wrong here.

Thanks,
Evan 

PS - Using Ecto v2.0.0-beta.1

ev...@sherwood.io

unread,
Mar 3, 2016, 3:11:28 AM3/3/16
to elixir-ecto
Gah, I got it working after much trial-and-error and studying the tests here: https://github.com/elixir-lang/ecto/blob/430545ceaf9506b231203f1c148e174d11a9ecb4/integration_test/cases/assoc.exs

This is what I'm doing now:

def update(conn, %{"id" => id, "dish" => dish_params}, vendor) do
  dish
= Repo.get!(vendor_dishes(vendor), id)

  dietary_prefs
= dish_params["dietary_prefs"]
 
|> parse_dietary_pref_ids
 
|> get_dietary_prefs_with_ids

 
case (Repo.transaction(fn ->
   
{ :ok, updated_dish } = dish
   
|> Repo.preload(:dietary_prefs)
   
|> Ecto.Changeset.change(dish_params)
   
|> Ecto.Changeset.put_assoc(:dietary_prefs, [])
   
|> Repo.update

    updated_dish
   
|> Ecto.Changeset.change
   
|> Ecto.Changeset.put_assoc(:dietary_prefs, dietary_prefs)
   
|> Repo.update
 
end)) do
   
{ :ok, { :ok, dish }} ->
      render
(conn, "show.json", dish: dish)
   
{ :ok, { :error, changeset }} ->
      conn
     
|> put_status(:unprocessable_entity)
     
|> render(ChangesetView, "error.json", changeset: changeset)
 
end
end

Feels...weird. Is there a better way of doing this??

José Valim

unread,
Mar 3, 2016, 5:16:48 AM3/3/16
to elixi...@googlegroups.com
I will improve the error message but I believe your first example would work fine as long as you convert them to changesets as said in the error message:

from(in Dish, where: d.id == 20, preload: [:vendor, :dietary_prefs]) |> Repo.first |> Dish.changeset(%{}) |>Ecto.Changeset.put_assoc(:dietary_prefs, dietary_prefs)

Where

dietary_prefs = from(dp in Mp.DietaryPref, where: dp.id in ^[1,2]) |> Repo.all |> Enum.map(&Ecto.Changeset.change/1)

Notice the Enum.map at the end of the second pipeline. Could you please give this approach a try? The reason is because if you are giving structs, it is really hard for us to know what you are actually changing. If you give us a changeset instead, we will take it at face value. Thank you!

José Valim
Skype: jv.ptec
Founder and Director of R&D

--
You received this message because you are subscribed to the Google Groups "elixir-ecto" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-ecto...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-ecto/07339e5f-8ea6-4bb6-8147-07ac6d899a88%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

ev...@sherwood.io

unread,
Mar 3, 2016, 5:05:02 PM3/3/16
to elixir-ecto, jose....@plataformatec.com.br
Thank you, José! That was the secret sauce I was missing. I think I fell victim to the Rails-mindset of "assign these records/structs as the new values for the associations" and didn't register that the changesets need to be generated on the associations themselves **as well as** the parent item.

Things are working perfectly now. Thanks again!
Reply all
Reply to author
Forward
0 new messages