Need help with many to many relationship insert

Hello everyone,

I absolutely hit a roadblock and might need some help.

I have two schemas that are in a many-to-many relationship to each other. Here is a trimmed down version of both schemas:

Subtasks:

schema "subtasks" do
    field :description, :string
    field :status, :string
    field :title, :string

    many_to_many :tasks, SomeProject.Task.Task, join_through: "task_subtasks"

    timestamps()
  end

Tasks:

schema "tasks" do
    field :title, :string
    field :start_date, :string
    field :end_date, :string

    many_to_many :subtasks, SomeProject.Task.SubTask, join_through: "task_subtasks"

    timestamps()
  end

Now I try to write a test driven create method.

My input data (that I get from a Phoenix form) looks like this:

@valid_create_attrs %{
    title: "Task 1",
    start_date: "01.01.2020",
    end_date: "01.05.2020",
    subtasks: [
      %{
        title: "Exercise 1",
        description: "Some Text 1",
        status: "open"
      },
      %{
        title: "Exercise 2",
        description: "Some Text 2",
        status: "finished"
      }
    ]
  }

So I know that I have to send the struct through a changeset and also create a association via build_assoc but I’m absolutely not sure how this method might look like.

Any help is appreciated!

Edit: I’m tinkering around in the iex shell because I think a possible solution might be to perform pattern matching on the map. So I basically retrieve everything except the tasks entry to create the task changeset. Then I extract the tasks list and create one changeset for each item. Unfortunately I’m currently not able to perform the pattern matching…

Ecto.Changeset.cast_assoc/3 is a good place to start - you can pass parameters shaped like @valid_create_attrs above directly to your Task changeset without any additional logic in the controller.

Hey! I’m doing some progress.

Regarding cast_assoc I’ve read an article about the associations in Ecto: Link which states that build_assoc might be the correct function since I create a new Task + Subtasks. I use this create method for my seed script in my Phoenix app.

I found out how to shape the data like his:

def create_task!(attrs) do
    {subtasks, task} = Map.pop(attrs, :subtasks)

    %Task{}
    |> Task.changeset(task)
    |> Repo.insert!(on_conflict: :nothing)
  end

The subtasks are currently missing because I have to figure out how to insert a list that isn’t a queryable. Basically I have to bulk convert the maps inside the list to changesets and then insert them (that’s what I think).

Can you show what you have tried already, and the error you were getting?

Something like this is a good start:

Ecto.Changeset.cast(%Task{}, @valid_create_attrs, Task.__schema__(:fields))
|> Ecto.Changeset.cast_assoc(:subtasks, required: true)
|> Repo.insert!

However I think the data you have incoming needs to be in a different state:

To further complicate things, remember cast_assoc expects a particular shape of data that reflects your associations. In this case, because of the intermediate schema, the data sent through your forms in Phoenix would have to look as follows:

%{"todo_list" => %{
  "title" => "shipping list",
  "todo_list_items" => %{
    0 => %{"todo_item" => %{"description" => "bread"}},
    1 => %{"todo_item" => %{"description" => "eggs"}},
  }
}}

I wonder if you have to do this:

@valid_create_attrs %{
    title: "Task 1",
    start_date: "01.01.2020",
    end_date: "01.05.2020",
    subtasks: %{
     0 => %{
        title: "Exercise 1",
        description: "Some Text 1",
        status: "open"
      },
     1 => %{
        title: "Exercise 2",
        description: "Some Text 2",
        status: "finished"
      }
    }
  }