William
William

Reputation: 4588

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

Upvotes: 0

Views: 535

Answers (2)

William
William

Reputation: 4588

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})

Upvotes: 0

Afzal K.
Afzal K.

Reputation: 923

The correct syntax to manually create an association between a pre-existing Testbed and a Group is to

  1. Retrieve the group you want to assign the testbed to using its ID:

    group = App.Groups.get_group!(1)

  2. Retrieve the testbed you want to assign to the group using its ID:

    testbed = App.Testbeds.get_testbed!(1)

  3. Build the association:

    thing = Ecto.build_assoc(testbed, :group, group)

  4. Insert the association into the database using Repo.insert:

    Repo.insert(thing)

This will create the association between the testbed and the group, and the group_id field in the testbed will be updated with the ID of the group.

If you are receiving error when trying to insert a new Group, it means there must be a duplicate name in the database

adding an unique_constraint to the changeset options will solve this issue, as this will add the constraint as an error and prevent the duplicate value from being inserted into the database.

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

or you can check for duplicate names before inserting or updating the record using Ecto's Repo module.

Upvotes: 0

Related Questions