Simple belongs_to/has_many associations with Phoenix

Created: April 27, 2019 20:22 | Updated: April 28, 2019 07:07
Tags: Elixir, Phoenix

Coming from a Rails background, there are a few gotchas that I had to work through to make the magic happen. For this example I am using exercises and sets. Each set belongs to an exercise and each exercise has many sets. The app I created is called associations.

Generate all the things

First comes the scaffolding (don't forget to add the routes and migrate)

mix phx.gen.html Workouts Exercise exercises title:string
mix phx.gen.html Workouts Set sets reps:integer weight:float exercise_id:references:exercises

Schema modifications

Your exercise.ex schema needs to have its has_many added under schema.

# exercise.ex
defmodule Associations.Workouts.Exercise do
. . .
  schema "exercises" do
    field :title, :string
    has_many :sets, Associations.Workouts.Set

    timestamps()
  end
. . .
end

And your set.ex needs the belongs_to added to the schema and the exercise_id added to the changeset. I find it interesting that I no longer need to specify the exercise_id field, it is inferred as part of the belongs_to

# set.ex

defmodule Associations.Workouts.Set do
  use Ecto.Schema
  import Ecto.Changeset

  schema "sets" do
    field :reps, :integer
    field :weight, :float
    belongs_to :exercise, Associations.Workouts.Exercise

    timestamps()
  end

  @doc false
  def changeset(set, attrs) do
    set
    |> cast(attrs, [:reps, :weight, :exercise_id])
    |> validate_required([:reps, :weight, :exercise_id])
  end
end

Add exercise to set form

This is what your tag looks like, the @exercises is a map where each key is the title and value is the corresponding id.

# set/form.html.eex

  <%= select f, :exercise_id, @exercises %>
  <%= error_tag f, :exercise_id %>

Modify set controller

Your new and edit actions need to pass the exercises variable to your templates.

#set_controller.ex

defmodule AssociationsWeb.SetController do
. . .
  def new(conn, _params) do
    exercises = Workouts.exercise_map
    changeset = Workouts.change_set(%Set{})
    render(conn, "new.html", changeset: changeset, exercises: exercises)
  end
. . .
  def edit(conn, %{"id" => id}) do
    set = Workouts.get_set!(id)
    exercises = Workouts.exercise_map
    changeset = Workouts.change_set(set)
    render(conn, "edit.html", set: set, changeset: changeset, exercises: exercises)
  end
. . .
end

Workouts

Here you need to define your exercise_map function referenced earlier, and also preload the exercise in two others.

# workouts.ex

defmodule Associations.Workouts do
. . .
  def exercise_map do
    list_exercises()
    |> Enum.map(fn x -> {x.title, x.id} end)
  end
. . .
  def list_sets do
    Set
    |> preload(:exercise)
    |> Repo.all()
  end
. . .
  def get_set!(id) do
    Set
    |> preload(:exercise)
    |> Repo.get!(id)
  end
. . .
end

Use it in your views

Cool! Now you can display it. (This is clunky, but hey! I'm new!)

  <%= if @set.exercise_id do %>
  <li>
    <strong>Exercise:</strong>
    <%= @set.exercise.title %>
  </li>
  <% end %>

Written by Alan Vardy. Let me know how I can make this better!