0
votes

In my Phoenix app, I am getting a no function clause matching in Ecto.Changeset.change/2 error when trying to update a model with an embeds_many relationship. I've read the docs and seen other posts about this, but I can't figure out what I'm doing wrong.

First off, here's the error:

** (FunctionClauseError) no function clause matching in Ecto.Changeset.change/2
    (ecto) lib/ecto/changeset.ex:307: Ecto.Changeset.change(%{"content" => "<p>Nice to see you</p>", "duration" => 15, "id" => "93387d2d-a6ed-4902-911f-4dc1525aca2b"}, %{})
    (ecto) lib/ecto/changeset/relation.ex:196: Ecto.Changeset.Relation.on_replace/2
    (ecto) lib/ecto/changeset/relation.ex:299: Ecto.Changeset.Relation.reduce_delete_changesets/5
    (ecto) lib/ecto/changeset.ex:691: Ecto.Changeset.cast_relation/4
    (myapp) web/models/agenda.ex:20: MyApp.Agenda.changeset/2

The 'parent' model is Agenda, and the embedded model is AgendaPage. The models are defined as follows:

agenda.ex

defmodule MyApp.Agenda do
  use MyApp.Web, :model

  @primary_key {:id, :string, []}
  @derive {Phoenix.Param, key: :id}
  schema "agenda" do
    field :name, :string
    embeds_many :pages, MyApp.AgendaPage, on_replace: :delete
  end

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

agenda_page.ex

defmodule MyApp.AgendaPage do
  use MyApp.Web, :model

  embedded_schema do
    field :content, :string
    field :duration, :integer
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:content, :duration])
  end
end

And the update action from agenda_controller.ex

def update(conn, %{"id" => id, "agenda" => agenda_params}) do
  agenda = Repo.get!(Agenda, id)
  changeset = Agenda.changeset(agenda, agenda_params)

  case Repo.update(changeset) do
    {:ok, agenda} ->
      json conn, %{status: "ok", agenda: agenda}
    {:error, changeset} ->
      errors = parse_errors(changeset)
      IO.inspect errors
      json(conn |> put_status(400), %{status: "error", message: "Failed to update Agenda", errors: errors})
  end
end

In the iex terminal, I can access an existing agenda with MyApp.Repo.get(MyApp.Agenda, "default_agenda"), which gives back the following record:

%MyApp.Agenda{__meta__: #Ecto.Schema.Metadata<:built, "agenda">,
 id: "default_agenda", name: "Default Agenda",
 pages: [%{"content" => "<p>This is the default agenda</p>", "duration" => 10,
"id" => "0849862a-0794-4466-88a3-6052da360ca0"},
%{"content" => "<p>Nice to see you</p>", "duration" => 15,
"id" => "93387d2d-a6ed-4902-911f-4dc1525aca2b"}]}

An example of the agenda_params that would be passed into the changeset in the controller action would look like:

%{
  "id" => "default_agenda",
  "name" => "Default Agenda",
  "pages" => [
    %{
      "content" => "<p>foo</p>",
      "duration" => 10,
      "id" => "0849862a-0794-4466-88a3-6052da360ca0"
    },
    %{
      "content" => "<p>bar</p>",
      "duration" => 15,
      "id" => "93387d2d-a6ed-4902-911f-4dc1525aca2b"
    }
  ]
}

But trying to run this data through my update action produces the error. Can anyone offer some guidance?

1

1 Answers

0
votes

First off, the following does not look right:

%MyApp.Agenda{__meta__: #Ecto.Schema.Metadata<:built, "agenda">,
 id: "default_agenda", name: "Default Agenda",
 pages: [%{"content" => "<p>This is the default agenda</p>", "duration" => 10,
"id" => "0849862a-0794-4466-88a3-6052da360ca0"},
%{"content" => "<p>Nice to see you</p>", "duration" => 15,
"id" => "93387d2d-a6ed-4902-911f-4dc1525aca2b"}]}

The pages list should be a list of AgendaPage structs, not maps as you can see from my example below. I'm not sure how you ended up with that.

However, the main issue seems to be providing the id field in your params. I had the same issue when trying to update an existing record based on your sample params above. Hoever, if you drop the id field from the params on the update, it works.

Here is a working example:

iex(4)> {:ok, ag} = Repo.insert Agenda.changeset(%Agenda{id: "default_agenda"}, %{name: "Default Agenda"})
[debug] QUERY OK db=9.7ms
INSERT INTO "agendas" ("id","name","inserted_at","updated_at") VALUES ($1,$2,$3,$4) ["default_agenda", "Default Agenda", {{2017, 7, 21}, {23, 43, 46, 739178}}, {{2017, 7, 21}, {23, 43, 46, 747000}}]
{:ok,
 %Playground.Demo.Agenda{__meta__: #Ecto.Schema.Metadata<:loaded, "agendas">,
  id: "default_agenda", inserted_at: ~N[2017-07-21 23:43:46.739178],
  name: "Default Agenda", pages: [],
  updated_at: ~N[2017-07-21 23:43:46.747000]}}
iex(5)> Repo.update Agenda.changeset(ag, %{
...(5)>   "name" => "Default Agenda",
...(5)>   "pages" => [
...(5)>     %{
...(5)>       "content" => "<p>foo</p>",
...(5)>       "duration" => 10,
...(5)>       "id" => "0849862a-0794-4466-88a3-6052da360ca0"
...(5)>     },
...(5)>     %{
...(5)>       "content" => "<p>bar</p>",
...(5)>       "duration" => 15,
...(5)>       "id" => "93387d2d-a6ed-4902-911f-4dc1525aca2b"
...(5)>     }
...(5)>   ]
...(5)> }
...(5)> )
[debug] QUERY OK db=15.7ms
UPDATE "agendas" SET "pages" = $1, "updated_at" = $2 WHERE "id" = $3 [[%{content: "<p>foo</p>", duration: 10, id: "e7b5b7d9-9308-43f0-8bc7-c5956640772b"}, %{content: "<p>bar</p>", duration: 15, id: "1c668d06-5c60-4a4d-a052-43520597162d"}], {{2017, 7, 21}, {23, 44, 23, 752892}}, "default_agenda"]
{:ok,
 %Playground.Demo.Agenda{__meta__: #Ecto.Schema.Metadata<:loaded, "agendas">,
  id: "default_agenda", inserted_at: ~N[2017-07-21 23:43:46.739178],
  name: "Default Agenda",
  pages: [%Playground.Demo.AgendaPage{content: "<p>foo</p>", duration: 10,
    id: "e7b5b7d9-9308-43f0-8bc7-c5956640772b"},
   %Playground.Demo.AgendaPage{content: "<p>bar</p>", duration: 15,
    id: "1c668d06-5c60-4a4d-a052-43520597162d"}],
  updated_at: ~N[2017-07-21 23:44:23.752892]}}
iex(6)> Repo.get Agenda, "default_agenda"
[debug] QUERY OK source="agendas" db=7.2ms
SELECT a0."id", a0."name", a0."pages", a0."inserted_at", a0."updated_at" FROM "agendas" AS a0 WHERE (a0."id" = $1) ["default_agenda"]
%Playground.Demo.Agenda{__meta__: #Ecto.Schema.Metadata<:loaded, "agendas">,
 id: "default_agenda", inserted_at: ~N[2017-07-21 23:43:46.739178],
 name: "Default Agenda",
 pages: [%Playground.Demo.AgendaPage{content: "<p>foo</p>", duration: 10,
   id: "e7b5b7d9-9308-43f0-8bc7-c5956640772b"},
  %Playground.Demo.AgendaPage{content: "<p>bar</p>", duration: 15,
   id: "1c668d06-5c60-4a4d-a052-43520597162d"}],
 updated_at: ~N[2017-07-21 23:44:23.752892]}
iex(7)>