elixirphoenix-frameworkecto

How to create a simple association between two tables in Phoenix and submit the data


I am learning how to configure a belong_to / has_many relationship between two tables. I have the configuration complete but I don't know the syntax to invoke it. I want to take a pre-existing Table called Testbed and assign it to another Table called Group.

This is a document I am writing to record the process , so I walk through most of the steps to where I am now.

In Phoenix I generated the two tables and related UI code using these commands:

mix phx.gen.html Testbeds Testbed testbeds name:string

mix phx.gen.html Groups Group groups name:string

The command line prompted me to add the resources / route for both of them. I did what it said.

I ran:

mix ecto.migrate

Testbed Migration

For testbeds, I create a migration file and add this field: :group_id, references(:groups)

defmodule App.Repo.Migrations.TestbedUpdate do
  use Ecto.Migration

  def change do
    alter table(:testbeds) do    
      add :group_id, references(:groups) # added
    end
  end
end

Change Group Schema

I update the Group schema. My changes are noted in Comments

defmodule App.Groups.Group do
  use Ecto.Schema
  import Ecto.Changeset
  
  alias App.Testbeds.Testbed   # Alias!
  schema "groups" do
    field :name, :string
    has_many :testbeds, Testbed    # Has many
    timestamps()
  end

  @doc false
  def changeset(group, attrs) do
    group
    |> cast(attrs, [:name])
    |> validate_required([:name])
  end
end

Change Testbed Schema

defmodule App.Testbeds.Testbed do
  use Ecto.Schema
  import Ecto.Changeset
  alias App.Groups.Group      # Alias
  schema "testbeds" do
    field :name, :string
    belongs_to :group, Group    # Belongs to
    timestamps()
  end

  @doc false
  def changeset(testbed, attrs) do
    testbed
    |> cast(attrs, [:name])
    |> validate_required([:name])
  end
end

I read that I needed to incorporate “preload” so that I can read data. So I did this:

  def list_testbeds do
    Repo.all(Testbed)
    |> Repo.preload([:group])
  end

and this

  def list_groups do
    Repo.all(Group)
     |> Repo.preload([:testbeds])
  end

When I create Groups and Testbeds and I query them, the result looks like this:

%App.Groups.Group{
    __meta__: #Ecto.Schema.Metadata<:loaded, "groups">,
    id: 3,
    name: "FUNK",
    testbeds: [],
    inserted_at: ~N[2023-09-27 18:13:15],
    updated_at: ~N[2023-09-27 18:13:15]
  }



%App.Testbeds.Testbed{
    __meta__: #Ecto.Schema.Metadata<:loaded, "testbeds">,
    id: 1,
    name: "Bloop",
    group_id: nil,
    group: nil,
    inserted_at: ~N[2023-09-27 14:04:15],
    updated_at: ~N[2023-09-27 18:22:23]
  }

I now need to know how to write code that assigns a Testbed to a Group.

Create a Group

App.Groups.create_group(%{name: "OINK"})

Submit Testbed and Assign to a Group Association

This code will create a new testbed and assign it to the Group that has an ID of 1.

group = App.Groups.get_group!(1)
thing = Ecto.build_assoc(group, :testbeds, name: "DUMB")
alias App.{Repo}
Repo.insert(thing)

When you query the group you will see one with a field name testbeds that is assigned a List. The List contains a Testbed struct.

So now my question, if I want to take a pre-created testbed and make the same kind of association that the previous commands did, what do I do? I simply don't know the syntax and everything that I've tried leads to error.

I've tried to create a testbed and assign it a group_id manually like this:

App.Testbeds.update_testbed(my_testbed,%{group_id: 2,name: "some-name"}) 

It doesn't affect the group_id and it remains nil.

EDIT NOTE

These are the Testbed Function Created Per the Generator

defmodule App.Testbeds do
  @moduledoc """
  The Testbeds context.
  """

  import Ecto.Query, warn: false
  alias App.Repo

  alias App.Testbeds.Testbed

  @doc """
  Returns the list of testbeds.

  ## Examples

      iex> list_testbeds()
      [%Testbed{}, ...]

  """
  def list_testbeds do
    Repo.all(Testbed)
    |> Repo.preload([:group])
  end

  @doc """
  Gets a single testbed.

  Raises `Ecto.NoResultsError` if the Testbed does not exist.

  ## Examples

      iex> get_testbed!(123)
      %Testbed{}

      iex> get_testbed!(456)
      ** (Ecto.NoResultsError)

  """
  def get_testbed!(id), do: Repo.get!(Testbed, id)

  @doc """
  Creates a testbed.

  ## Examples

      iex> create_testbed(%{field: value})
      {:ok, %Testbed{}}

      iex> create_testbed(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_testbed(attrs \\ %{}) do
    %Testbed{}
    |> Testbed.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a testbed.

  ## Examples

      iex> update_testbed(testbed, %{field: new_value})
      {:ok, %Testbed{}}

      iex> update_testbed(testbed, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_testbed(%Testbed{} = testbed, attrs) do
    testbed
    |> Testbed.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a testbed.

  ## Examples

      iex> delete_testbed(testbed)
      {:ok, %Testbed{}}

      iex> delete_testbed(testbed)
      {:error, %Ecto.Changeset{}}

  """
  def delete_testbed(%Testbed{} = testbed) do
    Repo.delete(testbed)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking testbed changes.

  ## Examples

      iex> change_testbed(testbed)
      %Ecto.Changeset{data: %Testbed{}}

  """
  def change_testbed(%Testbed{} = testbed, attrs \\ %{}) do
    Testbed.changeset(testbed, attrs)
  end
end

Solution

  • I needed to update the schema to reference the group_id.

    defmodule App.Testbeds.Testbed do
      use Ecto.Schema
      import Ecto.Changeset
      alias App.Groups.Group      # Alias
      schema "testbeds" do
        field :name, :string
        belongs_to :group, Group    # Belongs to
        timestamps()
      end
    
      @doc false
      def changeset(testbed, attrs) do
        testbed
        |> cast(attrs, [:name, :group_id])   # group_id <--- Answer
        |> validate_required([:name])
      end
    end
    

    Then performing these commands adds the testbed to the group

    group = App.Groups.get_group!(1)
    item = App.Items.get_item!(1)
    App.Items.update_item(item, %{group_id: group.id})