arunkarottu

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

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

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?

Where Next?

Popular in Questions Top

hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 130286 1222
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

We're in Beta

About us Mission Statement