How can I make a many to many relation with ecto 2? As an example app I want to create a Post which can be in multiple categories. The categories already exist. For example:
[%Category{id: "1", name: "elixir"}, %Category{id: "2", name: "erlang"}]
Im using Ecto 2 beta 0. The example project is called Ecto2.
I defined two models:
defmodule Ecto2.Post do
use Ecto2.Web, :model
use Ecto.Schema
schema "posts" do
field :title, :string
many_to_many :categories, Ecto2.Category, join_through: "posts_categories", on_replace: :delete
timestamps
end
@required_fields ~w(title)
@optional_fields ~w()
def changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
|> cast_assoc(:categories) # not suitable?
end
end
defmodule Ecto2.Category do
use Ecto2.Web, :model
schema "categories" do
field :name, :string
timestamps
end
@required_fields ~w(name)
@optional_fields ~w()
def changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
end
end
I tried doing it like this:
post = Repo.get!(Post, 1) |> Repo.preload(:categories)
changeset = Post.changeset(post, %{"title"=> "bla", "categories"=> [%{id: "1"}]})
Repo.update!(changeset)
But cast_assoc in Post.changeset is not suitable for this task, it wants to create a whole new Category instead of associate one. What should I use instead? build_assoc? But build_assoc docs do not mention it is useful with many_to_many. How do I use it? Should I put build_assoc in Post.changeset then, or should i use it in a phoenix controller.
After a good night of sleep and some digging in the ecto unit tests i have found a partial answer. The right function to call is Ecto.Changeset.put_assoc. It returns a changeset. The rest of the question is on the bottom of this reply.
It is a partial solution, because if i want to update the related categories (Post already has a list of related categories) I have to remove and then save the empty list of categories first. Is it possible to do this in one go?
You can join through a table by passing a string like "posts_categories", or through a schema by passing through a schema like MyApp.PostCategory. I prefer joining through schema as timestamps can be included. Let say you choose join through a schema instead of a table:
```
```
Ecto beta 2 has changed :empty to empty map and change cast\4 to cast \3. Check changelog.
Add this line to your post schema:
many_to_many :categories, Ecto2.Category, join_through: Ecto2.PostCategory
Add this line to your category schema:
many_to_many :posts, Ecto2.Post, join_through: Ecto2.PostCategory
That's it! Now you can update like ```
```