My Phoenix 1.3 app has two simple schemas Post
and Tag
. Since the two schemas have a many-to-many relationship, I associate them using a third join schema, PostTag
, as suggested in the “What’s new with Ecto 2.0” book and this blog post about nested associations.
I was expecting to be able to create a new Post
and add new and existing tags to it by using a single nested Post
changeset, as described in the cast_assoc/4
documentation.
The test below describes my expectations:
test "create Post with existing and new Tags" do
tag =
tag_changeset(%Tag{}, %{name: "existing tag"})
|> Repo.insert!
tag_attrs = [
%{name: "existing tag - updated", id: tag.id},
%{name: "new tag"}
]
post_attrs = %{title: "some title", tags: tag_attrs}
assert {:ok, %Post{} = post} =
%Post{}
|> post_changeset(post_attrs)
|> Repo.insert()
assert post.title == "some title"
tags =
Ecto.assoc(post, :tags)
|> Repo.all
|> Enum.map(fn (t) -> {t.name, t.id} end)
|> Enum.into(%{})
assert is_integer(tags["new tag"])
assert tags["existing tag - updated"] == tag.id
end
defp post_changeset(%Post{} = post, attrs) do
post
|> cast(attrs, [:title])
|> cast_assoc(:tags, with: &tag_changeset/2)
|> validate_required([:title])
end
defp tag_changeset(%Tag{} = tag, attrs) do
tag
|> cast(attrs, [:name])
|> validate_required([:name])
end
The test fails with, and inserts two new Tags
instead of updating the name of one and adding a single new one.
Assertion with == failed
code: tags["existing tag - updated"] == tag.id()
left: 70
right: 69
My question is: Is this expected behaviour?
The documentation for cast_assoc/4
suggests that it is not expected behaviour:
If the parameter contains an ID and there is an associated child with such ID, the parameter data will be passed to changeset/2 with the existing struct and become an update operation
I can only guess that the documentation only contemplated a belongs_to
nesting, not a many_to_many
one.
FWIW, I am aware I can use put_assoc
to replace the entire association, but that’s not what I am trying to do here.
Here are the supporting schema modules; migrations omitted.
defmodule PhxSandbox.Blog.Post do
use Ecto.Schema
schema "blog_posts" do
field :title, :string
many_to_many :tags, PhxSandbox.Blog.Tag, join_through: PhxSandbox.Blog.PostTag
timestamps()
end
end
defmodule PhxSandbox.Blog.Tag do
use Ecto.Schema
schema "blog_tags" do
field :name, :string
many_to_many :posts, PhxSandbox.Blog.Post, join_through: PhxSandbox.Blog.PostTag
timestamps()
end
end
defmodule PhxSandbox.Blog.PostTag do
use Ecto.Schema
schema "blog_posts_tags" do
belongs_to :post, PhxSandbox.Blog.Post
belongs_to :tag, PhxSandbox.Blog.Tag
timestamps()
end
end