Updating all associated entries with Ecto

Hi alchemists,

I know that there are topics with exact same title but they don’t really address my question.

Setup:

  • two schemas: Round has many Matches
  • Match have result field
  • a form to set round results which write results to all Matches in a Round

Round:

schema "rounds" do
  many_to_many :matches, Match, join_through: "round_matches"
end

Match:

schema "matches" do
  field :result
end

Form:

h1 Finalize Round

= form_for @changeset, Routes.round_result_path(@conn, :result, @changeset.data), fn f ->
 
  = for match_fields <- inputs_for(f, :matches) do
  div
      = hidden_input match_fields, :id, value: match_fields.data.id
      = label match_fields, :result
      = select(match_fields, :result, ["Home Team": "home_team", "Draw": "draw", "Away Team": "away"])

  div
    = submit "Submit"

When submitted this form output that I split in matches and round_id in controller

matches = %{
      "0" => %{"id" => "7277d8e5-f239-4474-ada4-09b347e100aa", "result" => "home_team"},
      "1" => %{"id" => "ef63696e-dced-46b8-9f2c-edb52d17a93f", "result" => "home_team"},
      "10" => %{"id" => "6a3656d3-b756-4f45-9057-68a6d35bb5a3", "result" => "home_team"},
      "11" => %{"id" => "db1601f9-0609-45cf-81b4-220f99cd4df8", "result" => "home_team"},
      "12" => %{"id" => "0dbc35f0-a3a6-46f8-9391-9e80194f6303", "result" => "home_team"},
      "2" => %{"id" => "6cdb7765-60e3-4056-b33e-06e3512973c6", "result" => "home_team"},
      "3" => %{"id" => "8c53843c-ba35-44ca-a478-74459bac1c32", "result" => "home_team"},
      "4" => %{"id" => "c097502f-6e38-487e-9226-6220f89c13d7", "result" => "home_team"},
      "5" => %{"id" => "13b325a7-1723-4b24-a677-f9e2d0a16e5c", "result" => "home_team"},
      "6" => %{"id" => "3dfa9ea4-25e7-4381-9f0b-44ae58a757b8", "result" => "home_team"},
      "7" => %{"id" => "3b9833b9-baf3-4bd7-95fc-f670e4eb15fb", "result" => "home_team"},
      "8" => %{"id" => "1d00d1ff-e6c0-4372-acf0-287d56c67c84", "result" => "home_team"},
      "9" => %{"id" => "32fb6c1c-fff6-4dc7-836a-4dfc0aba17a3", "result" => "home_team"}
    }
   "round_id" = "917789de-56a2-4650-839c-17ff7d63fb3c"

Since I want to update all round matches I decided to go wtih put_assoc bit it raises an error errors: [matches: {"is invalid", [type: {:array, :map}]}] when fed a matches map.

 def result_changeset(round, matches_changes) do
    round
    |> cast(%{}, [])
    |> put_assoc(:matches, matches_changes)`

In contrast when I convert that map to a least of maps, each of which contains an id field and convert string keys to atom keys then everything seems to work fine

matches_changes =
      matches_changes
      |> Map.values()
      |> Enum.map(& Map.new(&1, fn {k, v} -> {String.to_atom(k), v} end))

round
    |> cast(%{}, [])
    |> put_assoc(:matches, matches_changes)

Am I doing something wrong? I’d expect put_assoc to be able to work with output produced by inputs_for without additional rituals.

Solution #1 - do not split inputs_for output in controller - pass it directly to cast and use cast_assoc

 round
    |> cast(matches_changes, [])
    |> cast_assoc(:matches, required: true, with: &Match.result_changeset/2)

The downside is that all Match should necessary be necessarily preloaded, while put_assoc has this check built in.

I created a relevant thread in Ecto mailing list https://groups.google.com/forum/#!topic/elixir-ecto/_29KgJlT2Ok

I’d say also add this schema:

schema "rounds_matches" do
  belongs_to :match, Match
  belongs_to :round, Round
end

And then probably add this:

Subsequently you can transform your HTTP parameters (coming to your controller) to RoundMatch changesets and insert those?

Oh, my - that’s a typo: I replaced many_to_many association with has_many:

schema "rounds" do
  has_many :matches, Match
end