arunkarottu
Cast_assoc issue with many_to_many
cast_assoc doesn’t seem to work when I create or update with an existing item in the many_to_many relationship and the item has a required field. See below for example:
defmodule EctoTest.Repo.Migrations.AddTodoListsAndItems do
use Ecto.Migration
def change do
create table("todo_lists") do
add(:title, :string, null: false)
timestamps()
end
create table("todo_items") do
add(:description, :string, null: false)
timestamps()
end
create table("todo_lists_items", primary_key: false) do
add(:todo_item_id, references(:todo_items), null: true)
add(:todo_list_id, references(:todo_lists), null: true)
end
end
end
defmodule EctoTest.Todos.TodoList do
use Ecto.Schema
alias Ecto.Changeset
alias EctoTest.Todos.TodoItem
schema "todo_lists" do
field :title
many_to_many :todo_items, TodoItem, join_through: "todo_lists_items", on_replace: :delete
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> Changeset.cast(params, [:title])
|> Changeset.cast_assoc(:todo_items, required: true)
end
end
defmodule EctoTest.Todos.TodoItem do
use Ecto.Schema
alias Ecto.Changeset
schema "todo_items" do
field :description
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> Changeset.cast(params, [:description])
|> Changeset.validate_required([:description])
end
end
defmodule EctoTest.Todos do
alias EctoTest.Repo
alias EctoTest.Todos.TodoItem
alias EctoTest.Todos.TodoList
def create_todo_item(attrs \\ %{}) do
%TodoItem{}
|> TodoItem.changeset(attrs)
|> Repo.insert()
end
def create_todo_list(attrs \\ %{}) do
%TodoList{}
|> TodoList.changeset(attrs)
|> Repo.insert()
end
def update_todo_list(%TodoList{} = todo_list, attrs \\ %{}) do
todo_list
|> Repo.preload([:todo_items])
|> TodoList.changeset(attrs)
|> Repo.update()
end
end
Both the following tests fail with similar errors
defmodule EctoTest.Todos.TodoListTest do
use EctoTest.DataCase
alias EctoTest.Todos
describe "create_todo_list/2" do
test "creates todo_list and casts todo_items" do
{:ok, todo_item} = Todos.create_todo_item(%{description: "Test description"})
todo_list_params = %{title: "Test title", todo_items: [%{id: todo_item.id}]}
assert {:ok, todo_list} = Todos.create_todo_list(todo_list_params)
end
end
describe "update_todo_list/2" do
test "updates todo_list and casts todo_items" do
{:ok, todo_item} = Todos.create_todo_item(%{description: "Test description"})
todo_list_params = %{title: "Test title", todo_items: [%{id: todo_item.id}]}
assert {:ok, todo_list} = Todos.create_todo_list(todo_list_params)
{:ok, new_todo_item} = Todos.create_todo_item(%{description: "New todo item"})
todo_list_params = %{todo_items: [%{id: new_todo_item.id}]}
assert {:ok, todo_list} = Todos.update_todo_list(todo_list, todo_list_params)
end
end
end
These are the errors I get
1) test update_todo_list/2 updates todo_list and casts todo_items (EctoTest.Todos.TodoListTest)
test/ecto_test/todos/todo_list_test.exs:15
match (=) failed
code: assert {:ok, todo_list} = Todos.create_todo_list(todo_list_params)
right: {:error,
#Ecto.Changeset<
action: :insert,
changes: %{
title: "Test title",
todo_items: [
#Ecto.Changeset<
action: :insert,
changes: %{},
errors: [
description: {"can't be blank", [validation: :required]}
],
data: #EctoTest.Todos.TodoItem<>,
valid?: false
>
]
},
errors: [],
data: #EctoTest.Todos.TodoList<>,
valid?: false
>}
stacktrace:
test/ecto_test/todos/todo_list_test.exs:19: (test)
2) test create_todo_list/2 creates todo_list and casts todo_items (EctoTest.Todos.TodoListTest)
test/ecto_test/todos/todo_list_test.exs:6
match (=) failed
code: assert {:ok, todo_list} = Todos.create_todo_list(todo_list_params)
right: {:error,
#Ecto.Changeset<
action: :insert,
changes: %{
title: "Test title",
todo_items: [
#Ecto.Changeset<
action: :insert,
changes: %{},
errors: [
description: {"can't be blank", [validation: :required]}
],
data: #EctoTest.Todos.TodoItem<>,
valid?: false
>
]
},
errors: [],
data: #EctoTest.Todos.TodoList<>,
valid?: false
>}
stacktrace:
test/ecto_test/todos/todo_list_test.exs:10: (test)
Is this expected behavior? If so, what’s the best way for me to make this work?
Most Liked
joaquinalcerro
So, just to get the error out out the way, they are because the TodoItem changeset has a required field :description
def changeset(struct, params \\ %{}) do
struct
|> Changeset.cast(params, [:description])
|> Changeset.validate_required([:description])
end
… and you are calling it with just the “id”
todo_list_params = %{title: "Test title", todo_items: [%{id: todo_item.id}]}
But even if you fix that, you are still going to run into trouble. cast_assoc requires you to handle both parent and child association at once. You should have a params structure similar to:
Please note that in your test, you are first saving the TodoItem and then adding it to a TodoList and this will not work because cast_assoc works with both at one.
%{ "title" => "Your Title",
"todo_items" => [
%{"id" => "1", "description" => "Your description 1"},
%{"id" => "2", "description" => "Your description 2"},
]
}
Your actual TodoList.changeset should work when you are creating the record but if you are updating it you have preload the TodoItem association first.
You can also reference the documentation for cast_assoc:
Hope this helps. Best regards,
Last Post!
jeremyjh
Yes I agree the create can’t work the way he has it. What about the update though? Shouldn’t the preloaded struct be passed to changeset function?
Popular in Questions
Other popular topics
Latest Phoenix Threads
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #hex
- #security









