Issue with Ecto query preloading

Hi all, I am reading the Programming Phoenix book and following the steps to implement the Rumbl webapp.

Then at some steps I try to add something custom to learn a bit more.

So in that application, I have a controller which renders a template like this:

Listing videos

<%= for video <- @videos do %>
  <td class="text-right">
    <%= link "Show", to: video_path(@conn, :show, video), class: "btn btn-default btn-xs" %>
    <%= link "Edit", to: video_path(@conn, :edit, video), class: "btn btn-default btn-xs" %>
    <%= link "Delete", to: video_path(@conn, :delete, video), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %>
  </td>
</tr>

<% end %>

User Url Title Description Category
<%= video.user_id %> <%= video.url %> <%= video.title %> <%= video.description %> <%= if category = video.category, do: category.name %>

<%= link “New video”, to: video_path(@conn, :new) %>

I added this line to render the categories

<%= if category = video.category, do: category.name %>

This is my controller
def index(conn, _params, user) do
videos = Repo.all(user_videos(user)) |> Repo.preload(:category)
render(conn, “index.html”, videos: videos)
end

defp user_videos(user) do
# query user videos
assoc(user, :videos)
end

And when I run a test I wrote, this one:

test “renders index.html”, %{conn: conn} do
videos = [%Rumbl.Video{id: “1”, title: “dogs”},
%Rumbl.Video{id: “2”, title: “cats”}]
content = render_to_string(Rumbl.VideoView, “index.html”,
conn: conn, videos: videos)

assert String.contains?(content, "Listing videos")
for video <- videos do
  assert String.contains?(content, video.title)
end

end

It complains with categories not being preloaded

** (KeyError) key :name not found in: #Ecto.Association.NotLoaded

I also tried this:

defp user_videos(user) do
query = (from u in user, select: u.videos, preload: [:category])
Repo.all(query)
end

But not working neither, so how can I preload them?

I thought I just needed to preload them at the pipe with |> Repo.preload(:category)

Also the strange thing is this started to crash with test, without the test it renders the index.html template without any issue

It doesn’t matter what you do in the controller, since this test tests the view, not the controller.

videos = [%Rumbl.Video{id: "1", title: "dogs"}, %Rumbl.Video{id: "2", title: "cats"}]

You need to preload the category for these videos (in the test itself). Here that could mean just hardcoding it like:

videos = [%Rumbl.Video{id: "1", title: "dogs", category: %Rumbl.Category{name: “Animals”}}, ...]

Thanks that works, is there a way to make it more elegant handling it at the template? like “if video.category is nil dont ask for it”

I’m wondering this as well. It’s becoming a bear to preload associations. I’m close to writing a assert_without_assoc function :wink:

There’s Ecto.assoc_loaded?

Thanks, but that’s not quite what I’m looking for.

This is more of what I’m looking for (or at least what I think I want :wink:)

user = %User{id: "foo", widgets: [])
u = Users.get_user!(user.id) # this won't have widgets preloaded
# %User{id: "foo", widgets: Ecto.Association.NotLoaded< :widgets is not loaded>}
assert user == u

# or maybe I need to write
assert_without_assoc user == u
# where this will ignore fields where the assoc is not loaded.

Maybe this can shed some light:

1 Like

Word, that makes sense, except that’s not how the test code from mix phx.gen.html|context is generated. In a pinch I compare ids, but that doesn’t feel right.

That code is meant as a starting point to get people going and not to be some holy grail of how things are to be done. Their focus lies quite a bit more on being accessable to people new to the framework.

That I talk about in the linked post: Compare IDs if you want to assert on the identity (Is it the same thing?). Compare fields to each other if you want to make sure creating / updating works correctly for them.

I have a little helper function that might be useful to you:

  def clear_associations(%{__struct__: struct} = schema) do
    struct.__schema__(:associations)
    |> Enum.reduce(schema, fn association, schema ->
      %{schema | association => build_not_loaded(struct, association)}
    end)
  end

  defp build_not_loaded(struct, association) do
    %{
      cardinality: cardinality,
      field: field,
      owner: owner
    } = struct.__schema__(:association, association)

    %Ecto.Association.NotLoaded{
      __cardinality__: cardinality,
      __field__: field,
      __owner__: owner
    }
  end

With that you could write a assert_matches_without_associations function. Just call clear_associations/1 on each input and then assert equality.

Although (in line with what @LostKobrakai is saying) I actually use this assert_ids_match/2 helper more:

  @doc """
  Helper for checking that for two structs, or two lists of structs have the
  same id keys
  """
  def assert_ids_match(list1, list2) when is_list(list1) and is_list(list2) do
    list1_ids =
      list1
      |> Enum.map(& &1.id)
      |> Enum.sort()

    list2_ids =
      list2
      |> Enum.map(& &1.id)
      |> Enum.sort()

    assert list1_ids == list2_ids
  end

  def assert_ids_match(%{id: id1}, %{id: id2}) do
    assert id1 == id2
  end
3 Likes

Awesome, thanks for the code! I’ll probably head the assert_ids_match route soon; it’s good enough for me.

1 Like