1
votes

I have a Meetup model that has_many :rsvps, and Rsvp model with belongs_to :user.

Here's my endpoint:

def index(conn, _params) do
  one_hour_ago = Timex.now
                 |> Timex.shift(hours: -1)
                 |> Timex.to_unix
  meetups = from(m in Meetup, where: m.timestamp >= ^one_hour_ago)
            |> Repo.all
            |> Repo.preload(:rsvps)
  render conn, meetups: meetups
end

and my view

def render("index.json", %{ meetups: meetups }) do
  render_many(meetups, ParrotApi.MeetupView, "meetup.json")
end

def render("meetup.json", %{ meetup: meetup }) do
  %{
    id: meetup.id,
    rsvped_users: Enum.map(meetup.rsvps, fn rsvp ->
      rsvp |> Repo.preload(:user)
      user = rsvp.user
      %{
        image_url: user.image_url,
        interests: user.interests,
      }
    end),
  }
end

However, it is failing with ** (Plug.Conn.WrapperError) ** (KeyError) key :image_url not found in: #Ecto.Association.NotLoaded<association :user is not loaded>

It feels wrong to be importing Repo to a view. Is there a way to do this in the controller?

1
How about doing |> Repo.preload(rsvps: [:user]) in the controller instead?Dogbert
that worked!!! wow! thanks!!bigpotato
I would suggest not to preload in views - it's a path to a lot of pain and performance issues. Views should be pure functions - without side effects - this makes them extremely easily testable, cacheable, etc. Your implementation also suffers from N+1 queries - for each rsvp you will query separately for the user. Doing this from controller would issue only one query.michalmuskala

1 Answers

4
votes

Ecto supports nested preloading for this purpose. You just need to change:

|> Repo.preload(:rsvps)

to:

|> Repo.preload(rsvps: [:user])

in the controller and all the RSVPs of all the Meetups will have their user field preloaded efficiently.


You should pretty much never call preload in a loop (for / Enum.each / Enum.map etc) as it defeats the main purpose of preloading -- avoiding N+1 queries.