Listing matches for users
Tags: elixir, liveview, phoenix, backend
Published: 2024-02-05 19:37:11

Last post we had to use IEx to see matches being inserted on the database. This time we are going to do a little bit of frontend and at the same time learn a bit more about Ecto and LiveView streams. Our goal is to make the user page list all their matches.

## Reviewing some Ecto concepts

First of all, we need a method that lists matches for a certain user. Since we established last time that the `Ranking` module will handle those, let's go there.

```elixir
@doc """
Returns the list of matches a certain user participated.

## Examples

    iex> list_matches_for_user(1)
    [%Match{}, ...]

"""
def list_matches_for_user(user_id) do
  Match
  |> or_where(user_a_id: ^user_id)
  |> or_where(user_b_id: ^user_id)
  |> order_by(desc: :id)
  |> Repo.all()
end
```

This is a simple Ecto query that starts as broad as "list me all the matches" but uses [or_where/3](https://hexdocs.pm/ecto/Ecto.Query.html#or_where/3) to list only when either  `user_a_id` or `user_b_id` is equal to `user_id`. Not only that we use [order_by/3](https://hexdocs.pm/ecto/Ecto.Query.html#order_by/3) to sort descendingly so the latest matches can shine first.

## Back to the tables

Back when we needed to show a list of users we opted to use the `<.table>` component. We will do so again. Go to `user_live/show.ex`.

```diff
defmodule ChampionsWeb.UserLive.Show do
  # some stuff here

  @impl true
  def handle_params(%{"id" => id}, _session, socket) do
    user = Accounts.get_user!(id)
+   matches = Ranking.list_matches_for_user(user.id)
+
    {:noreply,
     socket
     |> assign(:page_title, "Showing user #{user.email}")
-    |> assign(:user, user)}
+    |> assign(:user, user)
+    |> stream(:matches, matches)
+   }
  end

  # some stuff here
  @impl true
  def render(assigns) do
    ~H"""
    <.header>
      User <%= @user.id %>
      <:subtitle>This is a player on this app.</:subtitle>
    </.header>

    <.list>
      <:item title="Email"><%= @user.email %></:item>
      <:item title="Points"><span data-points><%= @user.points %></span></:item>
    </.list>

    <div :if={@current_user && @current_user.id != @user.id} class="my-4">
      <.button type="button" phx-click="concede_loss">I lost to this person</.button>
      <.button type="button" phx-click="concede_draw">Declare draw match</.button>
    </div>
+
+   <.table
+     id="matches"
+     rows={@streams.matches}
+   >
+     <:col :let={{_id, match}} label="User A"><%= match.user_a_id %></:col>
+     <:col :let={{_id, match}} label="User B"><%= match.user_b_id %></:col>
+     <:col :let={{_id, match}} label="Points"><%= match.result %></:col>
+   </.table>

    <.back navigate={~p"/users"}>Back to users</.back>
    """
  end
end
```

Now there you go, another boring table! Wait, it's even more boring because it doesn't even show user names! Let's fix that. I'd like to take this opportunity to teach you how **NOT** to do this and after show you a good way of doing this.

## How NOT to load nested information on Phoenix LiveView

We could just use `Accounts.get_user!` inside our render function and just get users when rendering.

```diff
<.table
  id="matches"
  rows={@streams.matches}
>
- <:col :let={{_id, match}} label="User A"><%= match.user_a_id %></:col>
+ <:col :let={{_id, match}} label="User A"><%= Accounts.get_user!(match.user_a_id).email %></:col>
- <:col :let={{_id, match}} label="User B"><%= match.user_b_id %></:col>
+ <:col :let={{_id, match}} label="User B"><%= Accounts.get_user!(match.user_b_id).email %></:col>
  <:col :let={{_id, match}} label="Points"><%= match.result %></:col>
</.table>
```

The above works but have you ever heard of the [N+1 problem](https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping)? That's bad. We are basically querying once for the matches then for each match two more queries, one for each user. Here's how my terminal logs appear after rendering that.

```sql
↳ ChampionsWeb.UserLive.Show.handle_params/3, at: lib/champions_web/live/user_live/show.ex:9
[debug] QUERY OK source="matches" db=5.0ms idle=374.9ms
SELECT m0."id", m0."result", m0."user_a_id", m0."user_b_id", m0."inserted_at", m0."updated_at" FROM "matches" AS m0 WHERE (m0."user_a_id" = $1) OR (m0."user_b_id" = $2) ORDER BY m0."id" DESC [2, 2]
↳ ChampionsWeb.UserLive.Show.handle_params/3, at: lib/champions_web/live/user_live/show.ex:10
[debug] Replied in 11ms
[debug] QUERY OK source="users" db=2.4ms idle=372.6ms
SELECT u0."id", u0."email", u0."hashed_password", u0."confirmed_at", u0."points", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [1]
↳ anonymous fn/3 in ChampionsWeb.UserLive.Show.render/1, at: lib/champions_web/live/user_live/show.ex:66
[debug] QUERY OK source="users" db=3.3ms decode=0.7ms idle=369.5ms
SELECT u0."id", u0."email", u0."hashed_password", u0."confirmed_at", u0."points", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [2]
↳ anonymous fn/3 in ChampionsWeb.UserLive.Show.render/1, at: lib/champions_web/live/user_live/show.ex:67
[debug] QUERY OK source="users" db=4.1ms idle=365.0ms
SELECT u0."id", u0."email", u0."hashed_password", u0."confirmed_at", u0."points", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [1]
...
```

To add fuel to the problem, every single time our render function is called we would run those queries again. So if something triggers an update in the template, say the points changed, we'd run a ton of load on our database. Don't do queries inside your HEEx!

## How to do load nested information on ~~Phoenix LiveView~~ Ecto

Ecto, just like any other good ORM, already knows that models can have relations to each other. We can teach it that a `Match` model belongs to each `User` model by simply changing the following:

```diff
schema "matches" do
  field :result, Ecto.Enum, values: [:winner_a, :winner_b, :draw]
- field :user_a_id, :integer
+ belongs_to :user_a, Champions.Accounts.User
- field :user_b_id, :integer
+ belongs_to :user_b, Champions.Accounts.User

  timestamps()
end
```

Implicetely when you use Ecto's [belongs_to/3](https://hexdocs.pm/ecto/Ecto.Schema.html#belongs_to/3) you get two fields in your model. If you say `belongs_to: :user_a, Champions.Accounts.User` a `user_a_id` field will be created with `:integer` type and also a `:user_a` field will be created which will store `User` when we preload it. Preloading relations in Ecto is quite easy. Go to your `Rankings`  context and add this line:

```diff
def list_matches_for_user(user_id) do
  Match
  |> or_where(user_a_id: ^user_id)
  |> or_where(user_b_id: ^user_id)
  |> order_by(desc: :id)
+ |> preload([:user_a, :user_b])
  |> Repo.all()
end
```

Now all matches will come with both fields pre-populated. We can use them like this:

```diff
<.table
  id="matches"
  rows={@streams.matches}
>
- <:col :let={{_id, match}} label="User A"><%= match.user_a_id %></:col>
+ <:col :let={{_id, match}} label="User A"><%= match.user_a.email %></:col>
- <:col :let={{_id, match}} label="User B"><%= match.user_b_id %></:col>
+ <:col :let={{_id, match}} label="User B"><%= match.user_b.email %></:col>
  <:col :let={{_id, match}} label="Points"><%= match.result %></:col>
</.table>
```

That's magic, ladies and gentleman. Our table is slightly less boring. One thing it doesn't do though is add matches as soon as someone clicks on the button. Let's do that.

## Learning a little bit more about Phoenix LiveView Streams

Whenever we run the functions that concede loss or declare a draw match we receive back the updated users so we can update our UI. We should do the same for the match that was just created so we can put that into our UI. Let's get back to the `Ranking` context.

```diff
  @doc """
  Adds 3 points to the winning user

  ## Examples

      iex> concede_loss_to(%User{points: 0})
-     {:ok, %User{points: 3}}
+     {:ok, %User{points: 3}, %Match{}}

  """
  def concede_loss_to(loser, winner) do
-   {:ok, _match} = create_match(%{
+   {:ok, match} = create_match(%{
      user_a_id: loser.id,
      user_b_id: winner.id,
      result: :winner_b
    })
-   increment_user_points(winner, 3)
+   {:ok, updated_user} = increment_user_points(winner, 3)
+   {:ok, updated_user, Repo.preload(match, [:user_a, :user_b])}
  end

  @doc """
  Adds 1 point to each user

  ## Examples

      iex> declare_draw_match(%User{points: 0}, %User{points: 0})
-     {:ok, %User{points: 1}, %User{points: 1}}
+     {:ok, %User{points: 1}, %User{points: 1}, %Match{}}

  """
  def declare_draw_match(user_a, user_b) do
-   {:ok, _match} = create_match(%{
+   {:ok, match} = create_match(%{
      user_a_id: user_a.id,
      user_b_id: user_b.id,
      result: :draw
    })
    {:ok, updated_user_a} = increment_user_points(user_a, 1)
    {:ok, updated_user_b} = increment_user_points(user_b, 1)
-   {:ok, updated_user_a, updated_user_b}
+   {:ok, updated_user_a, updated_user_b, Repo.preload(match, [:user_a, :user_b])}
  end
```

As you can see both functions now also send a %Match{} as the last element on the tuple. Now let's update our usages in the UI. It's very important to notice that we also preloaded the associations after creating the matches so our UI will have that info otherwise it's going to error later on.

```diff
  def handle_event("concede_loss", _value, %{assigns: %{current_user: current_user, user: user}} = socket) do
-   {:ok, updated_user} = Ranking.concede_loss_to(current_user, user)
-   {:noreply, assign(socket, :user, updated_user)}
+   {:ok, updated_user, match} = Ranking.concede_loss_to(current_user, user)
+
+   {:noreply,
+     socket
+     |> assign(:user, updated_user)
+     |> stream_insert(:matches, match, at: 0)
+   }
  end

  def handle_event("concede_draw", _value, %{assigns: %{current_user: current_user, user: user}} = socket) do
-   {:ok, updated_my_user, updated_user} = Ranking.declare_draw_match(current_user, user)
+   {:ok, updated_my_user, updated_user, match} = Ranking.declare_draw_match(current_user, user)
    {:noreply,
      socket
      |> assign(:user, updated_user)
      |> assign(:current_user, updated_my_user)
+     |> stream_insert(:matches, match, at: 0)
    }
  end
```

Now we have a new function here. Meet [stream_insert/4](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream_insert/4). Remember that whenever we defined tables we used `stream(socket, :stream_name, elements)`? Streams are an efficient way of handling large collections of data in LiveView without storing them in memory.

When the user opens the LiveView we render the matches stream once and leave it at that. Now that we have a new match we use `stream_insert` using the same stream name and saying that the position should be 0 so this is effectively a prepend. If you're wondering why we prepend to the table, remember our match table is sorted in descending ID order so new ones should go at the top. Feel free to test it and when you're comfortable we will start fixing some tests.

## Fixing old tests

Right now you should have at least a couple erroring tests. It's fine, we changed things on `Ranking` module so let's head to `test/champions/ranking_test.exs`.

```diff
  describe "concede_loss_to/2" do
    test "adds 3 points to the winner" do
      loser = user_fixture()
      user = user_fixture()
      assert user.points == 0
-     assert {:ok, %User{points: 3}} = Ranking.concede_loss_to(loser, user)
-     match = get_last_match!()
+     assert {:ok, %User{points: 3}, match} = Ranking.concede_loss_to(loser, user)
      assert match.user_a_id == loser.id
      assert match.user_b_id == user.id
      assert match.result == :winner_b
    end
  end

  describe "declare_draw_match/2" do
    test "adds 1 point to each user" do
      user_a = user_fixture()
      user_b = user_fixture()
      assert user_a.points == 0
      assert user_b.points == 0
-     assert {:ok, %User{points: 1}, %User{points: 1}} = Ranking.declare_draw_match(user_a, user_b)
-     match = get_last_match!()
+     assert {:ok, %User{points: 1}, %User{points: 1}, match} = Ranking.declare_draw_match(user_a, user_b)
      assert match.user_a_id == user_a.id
      assert match.user_b_id == user_b.id
      assert match.result == :draw
    end
  end

# some stuff

- def get_last_match!() do
-   Match
-   |> order_by(desc: :id)
-   |> limit(1)
-   |> Repo.one!()
- end
```

All of a sudden our `get_last_match!` function is useless! But it was nice since it taught you some Ecto concepts last time.

## Adding tests for the UI changes

You LiveView tests are probably not failing right now, but that doesn't mean they have good coverage. Let's add simple tests for the matches. Head out to `test/champions_web/live/user_live_test.exs`:

```diff
describe "Authenticated Show" do
  setup [:register_and_log_in_user]

  # stuff

- test "concede 3 points when I lose to another player", %{conn: conn, user: _user} do
+ test "concede 3 points when I lose to another player", %{conn: conn, user: user} do
    other_user = user_fixture()
    {:ok, show_live, _html} = live(conn, ~p"/users/#{other_user}")

    assert other_user.points == 0
    assert show_live |> element("button", "I lost to this person") |> render_click()
    assert element(show_live, "span[data-points]") |> render() =~ "3"
+   assert element(show_live, "#matches") |> render() =~ user.email
+   assert element(show_live, "#matches") |> render() =~ other_user.email
+   assert element(show_live, "#matches") |> render() =~ "winner_b"
    assert Accounts.get_user!(other_user.id).points == 3
  end

  test "concede 1 point to each user when there's a draw match", %{conn: conn, user: user} do
    other_user = user_fixture()
    {:ok, show_live, _html} = live(conn, ~p"/users/#{other_user}")

    assert user.points == 0
    assert other_user.points == 0
    assert show_live |> element("button", "Declare draw match") |> render_click()
    assert element(show_live, "span[data-my-points]") |> render() =~ "1"
    assert element(show_live, "span[data-points]") |> render() =~ "1"
+   assert element(show_live, "#matches") |> render() =~ user.email
+   assert element(show_live, "#matches") |> render() =~ other_user.email
+   assert element(show_live, "#matches") |> render() =~ "draw"
    assert Accounts.get_user!(user.id).points == 1
    assert Accounts.get_user!(other_user.id).points == 1
  end
end
```

This is a simple tricky because `<.table>`  will use an ID equal to the stream name so in this case `#matches` then we just check the user emails and the `result` to be `winner_b` and `draw`. In just a few lines we already added tests to check that our LiveView is reactive. Neat huh?

## Summary

- Do not do queries on your LiveView, use assigns
- Preload that whenever need and possible
- We learned a bit more about LiveView streams, namely [stream_insert/4](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream_insert/4)

See you next time!