Changeset to save changes to parent model with a has_many children and also has_many grandchildren

I apologize in advance if this is the wrong forum to ask this newbie phoenix/elixir question:

I’m trying to post/save changes from my front end to my Phoenix/Elixir back end of a model with a has_many relationship, each of which have a has_many relationship but It’s not saving.

Here are my three relevant models:

defmodule ContestDirectorApi.Roundscore do
  use ContestDirectorApi.Web, :model

  schema "roundscores" do
    field :totalroundscore, :float
    field :normalizedscore, :float
    belongs_to :contestregistration, ContestDirectorApi.Contestregistration
    belongs_to :round, ContestDirectorApi.Round

    has_many :maneuverscores, ContestDirectorApi.Maneuverscore

    timestamps
  end

  @required_fields ~w(totalroundscore normalizedscore)
  @optional_fields ~w()

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> cast_assoc(:maneuverscores, required: true)
  end
end

defmodule ContestDirectorApi.Maneuverscore do
  use ContestDirectorApi.Web, :model

  schema "maneuverscores" do
    field :totalscore, :float
    belongs_to :maneuver, ContestDirectorApi.Maneuver
    belongs_to :roundscore, ContestDirectorApi.Roundscore

    has_many :scores, ContestDirectorApi.Score

    timestamps
  end

  @required_fields ~w(totalscore)
  @optional_fields ~w()

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> cast_assoc(:scores, required: true)
  end
end

defmodule ContestDirectorApi.Score do
  use ContestDirectorApi.Web, :model

  schema "scores" do
    field :points, :float
    belongs_to :maneuverscore, ContestDirectorApi.Maneuverscore

    timestamps
  end

  @required_fields ~w(points)
  @optional_fields ~w()

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end
end

And my controller’s update method:

def update(conn, %{"id" => id, "data" => data = %{"type" => "roundscores", "attributes" => roundscore_params}}) do
  roundscore = Repo.get!(Roundscore, id) |> Repo.preload(:maneuverscores) |> Repo.preload(:contestregistration)
  changeset = Roundscore.changeset(roundscore, Params.to_attributes(data))

  case Repo.update(changeset) do
    {:ok, roundscore} ->
      render(conn, "show.json", data: roundscore)
    {:error, changeset} ->
      conn
      |> put_status(:unprocessable_entity)
      |> render(ContestDirectorApi.ChangesetView, "error.json", changeset: changeset)
  end
end

When I post my changes, the log shows:

[debug] Processing by ContestDirectorApi.RoundscoreController.update/2
  Parameters: %{"data" => %{"attributes" => %{"normalizedscore" => 3214.29, "totalroundscore" => 45}, "id" => "13", "maneuverscores" => [%{"data" => %{"attributes" => %{"total-score" => 12}, "id" => "77", "relationships" => %{"maneuver" => %{"data" => %{"id" => "26", "type" => "maneuvers"}}, "roundscore" => %{"data" => %{"id" => "13", "type" => "roundscores"}}}, "scores" => [%{"data" => %{"attributes" => %{"points" => 1}, "id" => "244", "relationships" => %{"maneuverscore" => %{"data" => %{"id" => "77", "type" => "maneuverscores"}}}, "type" => "scores"}}, %{"data" => %{"attributes" => %{"points" => 4}, "id" => "245", "relationships" => %{"maneuverscore" => %{"data" => %{"id" => "77", "type" => "maneuverscores"}}}, "type" => "scores"}}, %{"data" => %{"attributes" => %{"points" => 7}, "id" => "246", "relationships" => %{"maneuverscore" => %{"data" => %{"id" => "77", "type" => "maneuverscores"}}}, "type" => "scores"}}], "type" => "maneuverscores"}}, %{"data" => %{"attributes" => %{"total-score" => 15}, "id" => "78", "relationships" => %{"maneuver" => %{"data" => %{"id" => "27", "type" => "maneuvers"}}, "roundscore" => %{"data" => %{"id" => "13", "type" => "roundscores"}}}, "scores" => [%{"data" => %{"attributes" => %{"points" => 2}, "id" => "247", "relationships" => %{"maneuverscore" => %{"data" => %{"id" => "78", "type" => "maneuverscores"}}}, "type" => "scores"}}, %{"data" => %{"attributes" => %{"points" => 5}, "id" => "248", "relationships" => %{"maneuverscore" => %{"data" => %{"id" => "78", "type" => "maneuverscores"}}}, "type" => "scores"}}, %{"data" => %{"attributes" => %{"points" => 8}, "id" => "249", "relationships" => %{"maneuverscore" => %{"data" => %{"id" => "78", "type" => "maneuverscores"}}}, "type" => "scores"}}], "type" => "maneuverscores"}}, %{"data" => %{"attributes" => %{"total-score" => 18}, "id" => "79", "relationships" => %{"maneuver" => %{"data" => %{"id" => "28", "type" => "maneuvers"}}, "roundscore" => %{"data" => %{"id" => "13", "type" => "roundscores"}}}, "scores" => [%{"data" => %{"attributes" => %{"points" => 3}, "id" => "250", "relationships" => %{"maneuverscore" => %{"data" => %{"id" => "79", "type" => "maneuverscores"}}}, "type" => "scores"}}, %{"data" => %{"attributes" => %{"points" => 6}, "id" => "251", "relationships" => %{"maneuverscore" => %{"data" => %{"id" => "79", "type" => "maneuverscores"}}}, "type" => "scores"}}, %{"data" => %{"attributes" => %{"points" => 9}, "id" => "252", "relationships" => %{"maneuverscore" => %{"data" => %{"id" => "79", "type" => "maneuverscores"}}}, "type" => "scores"}}], "type" => "maneuverscores"}}], "relationships" => %{"contestregistration" => %{"data" => %{"id" => "7", "type" => "contestregistrations"}}, "round" => %{"data" => %{"id" => "13", "type" => "rounds"}}}, "type" => "roundscores"}, "id" => "13"}
  Pipelines: [:api]
[debug] SELECT r0."id", r0."totalroundscore", r0."normalizedscore", r0."contestregistration_id", r0."round_id", r0."inserted_at", r0."updated_at" FROM "roundscores" AS r0 WHERE (r0."id" = $1) [13] OK query=1.1ms
[debug] SELECT m0."id", m0."totalscore", m0."maneuver_id", m0."roundscore_id", m0."inserted_at", m0."updated_at" FROM "maneuverscores" AS m0 WHERE (m0."roundscore_id" IN ($1)) ORDER BY m0."roundscore_id" [13] OK query=0.5ms
[debug] SELECT c0."id", c0."pilotname", c0."contest_id", c0."pilotclass_id", c0."pilot_id", c0."inserted_at", c0."updated_at" FROM "contestregistrations" AS c0 WHERE (c0."id" IN ($1)) [7] OK query=1.1ms

any if I pry on the changeset in the controller, it says:

%Ecto.Changeset{action: nil,
 changes: %{normalizedscore: 3214.29, totalroundscore: 45.0}, constraints: [],
 errors: [], filters: %{},
 model: %ContestDirectorApi.Roundscore{__meta__: #Ecto.Schema.Metadata<:loaded>,
  contestregistration: %ContestDirectorApi.Contestregistration{__meta__: #Ecto.Schema.Metadata<:loaded>,
   contest: #Ecto.Association.NotLoaded<association :contest is not loaded>,
   contest_id: 1, id: 7, inserted_at: #Ecto.DateTime<2016-06-06T22:41:27Z>,
   pilot: #Ecto.Association.NotLoaded<association :pilot is not loaded>,
   pilot_id: 3,
   pilotclass: #Ecto.Association.NotLoaded<association :pilotclass is not loaded>,
   pilotclass_id: 1, pilotname: "Dan Monroe",
   roundscores: #Ecto.Association.NotLoaded<association :roundscores is not loaded>,
   updated_at: #Ecto.DateTime<2016-06-06T22:41:27Z>}, contestregistration_id: 7,
  id: 13, inserted_at: #Ecto.DateTime<2016-06-06T22:47:15Z>,
  maneuverscores: [%ContestDirectorApi.Maneuverscore{__meta__: #Ecto.Schema.Metadata<:loaded>,
    id: 77, inserted_at: #Ecto.DateTime<2016-06-06T22:47:15Z>,
    maneuver: #Ecto.Association.NotLoaded<association :maneuver is not loaded>,
    maneuver_id: 26,
    roundscore: #Ecto.Association.NotLoaded<association :roundscore is not loaded>,
    roundscore_id: 13,
    scores: #Ecto.Association.NotLoaded<association :scores is not loaded>,
    totalscore: 0.0, updated_at: #Ecto.DateTime<2016-06-06T22:47:15Z>},
   %ContestDirectorApi.Maneuverscore{__meta__: #Ecto.Schema.Metadata<:loaded>,
    id: 78, inserted_at: #Ecto.DateTime<2016-06-06T22:47:15Z>,
    maneuver: #Ecto.Association.NotLoaded<association :maneuver is not loaded>,
    maneuver_id: 27,
    roundscore: #Ecto.Association.NotLoaded<association :roundscore is not loaded>,
    roundscore_id: 13,
    scores: #Ecto.Association.NotLoaded<association :scores is not loaded>,
    totalscore: 0.0, updated_at: #Ecto.DateTime<2016-06-06T22:47:15Z>},
   %ContestDirectorApi.Maneuverscore{__meta__: #Ecto.Schema.Metadata<:loaded>,
    id: 79, inserted_at: #Ecto.DateTime<2016-06-06T22:47:15Z>,
    maneuver: #Ecto.Association.NotLoaded<association :maneuver is not loaded>,
    maneuver_id: 28,
    roundscore: #Ecto.Association.NotLoaded<association :roundscore is not loaded>,
    roundscore_id: 13,
    scores: #Ecto.Association.NotLoaded<association :scores is not loaded>,
    totalscore: 0.0, updated_at: #Ecto.DateTime<2016-06-06T22:47:15Z>}],
  normalizedscore: 1076.92,
  round: #Ecto.Association.NotLoaded<association :round is not loaded>,
  round_id: 13, totalroundscore: 14.0,
  updated_at: #Ecto.DateTime<2016-06-07T16:56:25Z>}, optional: [], opts: [],
 params: %{"contestregistration_id" => "7", "normalizedscore" => 3214.29,
   "round_id" => "13", "totalroundscore" => 45, "type" => "roundscores"},
 prepare: [], repo: nil,
 required: [:maneuverscores, :totalroundscore, :normalizedscore],
 types: %{contestregistration_id: :id, id: :id, inserted_at: Ecto.DateTime,
   maneuverscores: {:assoc,
    %Ecto.Association.Has{cardinality: :many, defaults: [],
     field: :maneuverscores, on_cast: :changeset, on_delete: :nothing,
     on_replace: :raise, owner: ContestDirectorApi.Roundscore, owner_key: :id,
     queryable: ContestDirectorApi.Maneuverscore,
     related: ContestDirectorApi.Maneuverscore, related_key: :roundscore_id}},
   normalizedscore: :float, round_id: :id, totalroundscore: :float,
   updated_at: Ecto.DateTime}, valid?: true, validations: []}

with a result that it does not save my changes to maneuverscores or scores.

I would really appreciate it if someone could point me in the right direction on how to save my relationship data for both relationships at the same time.

Thanks,
Dan

3 Likes

Hi @danmonroe,

Check this article (Manipulating associations and Nested associations and embeds), maybe you will find the answer there:

http://blog.plataformatec.com.br/2015/08/working-with-ecto-associations-and-embeds/

1 Like