toofarsideways
toofarsideways

Reputation: 3972

Inserting an model into a many-to-many relationship with an existing model in ecto 2

I'm trying out the Ecto 2 rc.

My models are:

schema "containers" do
  field :name, :string
  many_to_many :items,  Test.Item, join_through: Test.ContainerItem, on_delete: :delete_all

  timestamps
end

schema "items" do
  field :content, :string
  many_to_many :containers, Test.Container, join_through: Test.ContainerItem, on_delete: :delete_all

  timestamps
end

schema "containers_items" do
  belongs_to :container, Test.Container
  belongs_to :item, Test.Item

  timestamps
end

And my controller code is:

def add_item(conn, %{"item" => item_params, "container_id" => container_id}) do
  item = Item.changeset(%Item{}, item_params)
  IO.inspect(item) #TODO remove
  container = Container |> Repo.get(container_id) |> Repo.preload([:items])
  IO.inspect(container) #TODO remove

  changeset = container
    |> Ecto.Changeset.change
    |> Ecto.Changeset.put_assoc(:items, [item])

  IO.inspect(changeset) #TODO remove

  if changeset.valid? do
    Repo.update(changeset)

    conn
    |> put_flash(:info, "Item added.")
    |> redirect(to: container_path(conn, :show, container))
  else
    render(conn, "show.html", container: container, changeset: changeset)
  end
end

Now this works fine if I'm adding a single item to a container. However if an item exists on a container, then trying to add another item gives me:

(RuntimeError) you are attempting to change relation :items of Test.Container, but there is missing data.

I can't help but feel I'm going about this the wrong way, some advice would be appreciated.

Upvotes: 1

Views: 302

Answers (1)

toofarsideways
toofarsideways

Reputation: 3972

Ok, so I just figured this out.

My problem was is not turning the items into Changesets so that ecto can track the changes that it needs to make.

The only edits I needed to make are to the controller.

It should look like this instead:

def add_item(conn, %{"item" => item_params, "container_id" => container_id}) do
  item = Item.changeset(%Item{}, item_params)
  IO.inspect(item) #TODO remove
  container = Container |> Repo.get(container_id) |> Repo.preload([:items])
  IO.inspect(container) #TODO remove

  item_changesets = Enum.map([item | container.items], &Ecto.Changeset.change/1)

  changeset = container
    |> Ecto.Changeset.change
    |> Ecto.Changeset.put_assoc(:items, item_changesets)

  IO.inspect(changeset) #TODO remove

  if changeset.valid? do
    Repo.update(changeset)

    conn
    |> put_flash(:info, "Item added.")
    |> redirect(to: container_path(conn, :show, container))
  else
    render(conn, "show.html", container: container, changeset: changeset)
  end
end

Upvotes: 3

Related Questions