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
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.
def run_insert_1 do
c1 = Repo.get!(Category, 1)
c2 = %Category{name: "cat 2"}
# Inserting
changeset =
%Post{title: "1"}
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:categories, [c1, c2])
post = Repo.insert!(changeset)
IO.inspect post
end
def run_insert_2 do
c1 = Repo.insert! %Category{name: "cat 1"}
c2 = %Category{name: "cat 2"}
# Inserting
changeset =
%Post{title: "1"}
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:categories, [c1, c2])
post = Repo.insert!(changeset)
IO.inspect post
end
def run_update do
c1 = Repo.insert! %Category{name: "cat update"}
c2 = %Category{name: "cat 2"}
post = Repo.get!(Post, 1) |> Repo.preload(:categories)
# Updating
changeset =
post
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:categories, [c1])
post = Repo.update!(changeset)
IO.inspect post
end
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?
def run_update_2 do
c2 = Repo.get!(Tag, 2)
# Assumes Post 1 already has a few categories in it (for example after
# running run_update()
post = Repo.get!(Post, 1) |> Repo.preload(:categories)
# Remove and add again
changeset =
post
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:categories, [])
IO.inspect changeset
post = Repo.update!(changeset)
changeset =
post
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:categories, [c2])
post = Repo.update!(changeset)
IO.inspect post
end
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:
```
def change do
create table(:posts_categories) do
add :post_id, references(:posts)
add :category_id, references(:categories)
timestamps
end
end
```
defmodule Ecto2.PostCategory do
use Ecto2.Web, :model
schema "posts_categories" do
belongs_to :post, Ecto2.Post
belongs_to :category, Ecto2.Category
timestamps
end
def changeset(model, params \\ %{}) do
model
|> cast(params, [])
end
end
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 ```
post1 = Repo.get!(Post, 1)
category1 = Repo.get!(Category, 1)
post1
|> Repo.preload(:categories)
|> Post.changeset(%{})
|> put_assoc(:categories, [category1])
|> Repo.update!
```