Setting up a many to many relationship

Hi,

I’ve read some guides and the documentation, but I have not really grokked it yet.
I’m trying to set up a many to many relationship between Cameras and Summaries.
A Summary can have one or many Cameras.

I’ve created a table like this:

  def change do
    create table(:summaries_cameras, primary_key: false) do
      add :summary_id, references(:summaries, on_delete: :delete_all)
      add :camera_id, references(:cameras, on_delete: :delete_all)
    end

    create unique_index(:summaries_cameras, [:summary_id, :camera_id])
  end

And I’ve changed both modules like this:

  schema "summaries" do
    field :name, :string

    timestamps(type: :utc_datetime)

    many_to_many :cameras, MyApp.Cameras.Camera, join_through: "summaries_cameras"
  schema "cameras" do
    field :name, :string

    timestamps(type: :utc_datetime)

    many_to_many :cameras, MyApp.Cameras.Camera, join_through: "summaries_cameras"

So far so good, but I’m struggling to understand how I associate 2 cameras with a summary.

Here I’ve started on a test:

  test "associations" do
    camera1 = camera_fixture()
    camera2 = camera_fixture()
    summary = summary_fixture()

   # Make both cameras belong to the summary
   # What to do here?
  end

I think you meant to define it like this:

many_to_many :summaries, Summary, join_through: "summaries_cameras"

I don’t remember exactly how phoenix generated fixtures work, however with ExMachina you can do that easily by:

camera = build(:camera)
summary = build(:summary, cameras: [camera])

camera = build(:camera)
summary = build(:summary, cameras: [camera])

Do you know how to build the relationships outside the test environment?
How would I do it within my module?

You might want to take a look at cast_assoc/3 and put_assoc/4.

You can also go with the explicit approach, where you define a schema for the relations table and insert the relations explicitly in a transaction, highly depends on the use-case.

Thanks for sharing the links. I’ll check it out. :blush:

I think you can do it like this with fixtures:

  test "assosiations" do
 
    camera1 = camera_fixture()
    camera2 = camera_fixture()

    summary = summary_fixture(cameras: [camera1, camera2])

    summary = Repo.preload(summary, :cameras)
    assert summary.cameras == [camera1, camera2]

  end
end

  def summary_fixture(attrs \\ %{}) do
    {:ok, summary} =
      attrs
      |> Enum.into(%{
        name: 42,
      })
      |> MyApp.Wizard.create_summary()

    summary
  end

Here’s my failing tests:


  1) test assosiations (MyApp.WizardTest)
     test/myapp/wizard_test.exs:152
     Assertion with == failed
     code:  assert summary.cameras == [camera1, camera2]
     left:  []
     right: [
              %MyApp.Cameras.Camera{__meta__: #Ecto.Schema.Metadata<:loaded, "cameras">, id: 1, approved: false, name: "some name", notion_cover_image_url: nil, vendor_url: "some vendor_url", purchase_price: 120.5, sales_price: 120.5, is_ms_teams_certified: true, formula: 120.5, user_manual_url: nil, data_sheet_url: nil, component_vendor_id: 1, component_vendor: #Ecto.Association.NotLoaded<association :component_vendor is not loaded>, summaries: #Ecto.Association.NotLoaded<association :summaries is not loaded>, inserted_at: ~U[2024-09-06 04:32:02Z], updated_at: ~U[2024-09-06 04:32:02Z]},
              %MyApp.Cameras.Camera{__meta__: #Ecto.Schema.Metadata<:loaded, "cameras">, id: 2, approved: false, name: "some name", notion_cover_image_url: nil, vendor_url: "some vendor_url", purchase_price: 120.5, sales_price: 120.5, is_ms_teams_certified: true, formula: 120.5, user_manual_url: nil, data_sheet_url: nil, component_vendor_id: 2, component_vendor: #Ecto.Association.NotLoaded<association :component_vendor is not loaded>, summaries: #Ecto.Association.NotLoaded<association :summaries is not loaded>, inserted_at: ~U[2024-09-06 04:32:02Z], updated_at: ~U[2024-09-06 04:32:02Z]}
            ]
     stacktrace:
       test/easy_solutions/wizard_test.exs:163: (test)

I found an excellent guide at ElixirSchool that helped me assemble the pieces.

My code now looks like this:

The tests

  test "assosiations" do
    import MyApp.WizardFixtures
    import MyApp.CamerasFixtures
    alias MyApp.Wizard.Summary
    alias MyApp.Repo
    camera1 = camera_fixture()
    camera2 = camera_fixture()

    summary = summary_fixture()
    summary = Repo.preload(summary, [:cameras])
    summary_changeset = Ecto.Changeset.change(summary)

    summary_cameras_changeset = summary_changeset |> Ecto.Changeset.put_assoc(:cameras, [camera1, camera2])

    Repo.update!(summary_cameras_changeset)
    summary = Repo.reload(summary)
    summary = Repo.preload(summary, :cameras)
    assert summary.cameras == [camera1, camera2]

  end

The code

defmodule MyApp.SummaryCamera do
  use Ecto.Schema

  schema "summaries_cameras" do
    belongs_to :summary, MyApp.Wizard.Summary
    belongs_to :camera, MyApp.Cameras.Camera

    timestamps()
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> Ecto.Changeset.cast_assoc( :camera, required: true)
    |> Ecto.Changeset.cast_assoc( :summary, required: true)
  end
end

The migration:


  def change do
    create table(:summaries_cameras, primary_key: false) do
      add :summary_id, references(:summaries)
      add :camera_id, references(:cameras)
    end

    create unique_index(:summaries_cameras,
     [:summary_id, :camera_id])
  end

defmodule MyApp.Wizard.Summary do
  use Ecto.Schema
  import Ecto.Changeset

  schema "summaries" do
  
    many_to_many :cameras, MyApp.Cameras.Camera,
   join_through: "summaries_cameras"
defmodule MyApp.Wizard.Summary do
  use Ecto.Schema
  import Ecto.Changeset

  schema "summaries" do

  many_to_many :cameras, MyApp.Cameras.Camera, 
  join_through: "summaries_cameras"
3 Likes