1

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?

Afshin Moazami
  • 2,092
  • 5
  • 33
  • 55
bigpotato
  • 26,262
  • 56
  • 178
  • 334
  • 2
    How about doing `|> Repo.preload(rsvps: [:user])` in the controller instead? – Dogbert Apr 17 '17 at 09:20
  • that worked!!! wow! thanks!! – bigpotato Apr 17 '17 at 09:21
  • 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 Apr 17 '17 at 11:38

1 Answers1

3

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.

Community
  • 1
  • 1
Dogbert
  • 212,659
  • 41
  • 396
  • 397