yesyoukenn
yesyoukenn

Reputation: 227

How to load object relations that both exist and don't exist in Rails 4

I have 2 models: Comment and ReadReceipt. A Comment has many ReadReceipt for a given User. I want to create an ActiveRecord relation that would allow me Return all Comments with their ReadReceipt even if it doesn't exist. For example,

comments = Comment.includes(:read_receipts).all
comments.first.read_receipt -> #<ReadReceipt id: 1>
comments.last.read_receipt -> nil

Currently I have this #read_receipt on Comment. However, as I want to avoid N+1 queries I'm not sure what the best way to do this is. Do I need to do an left outer join? What's the Rails way to achieve this?

def read_receipt(user)
  receipt = ReadReceipt.find_by(feed_item_id: id, contact_id: user.contact.id)
  receipt ? true : false
end

Upvotes: 0

Views: 90

Answers (1)

kiddorails
kiddorails

Reputation: 13014

I assume your join column is feed_item_id in ReadReceipt.

For Rails 4, use includes and do LEFT OUTER JOIN manually, this will save you from N+1 queries and will also give all those comments where read receipt doesn't exist:

comments = Comment.includes(:read_receipts).joins('LEFT OUTER JOIN read_receipts on read_receipts.feed_item_id = comments.id')

comments.map do |c|
  c.read_receipt(some_user)
end

Because of includes earlier, read_receipts are already loaded in memory, using ActiveRecord for querying in read_receipt will run the query again with more parameters. You can use ruby instead if you want to get away with that. You can use loaded? to check if association is loaded or not.

Change your Comment#read_receipt to:

def read_receipt(user)
  # this will not load read receipts again in memory or fire any other query with subparameters
  if read_receipts.loaded? # this means association is already loaded, use Ruby
    read_receipts.find { |r| r.feed_item_id == id && r.contact_id == user.contact_id } ? true : false
  else
    # do not load all read_receipts, instead use proper ActiveRecord
    read_receipts.find_by(contact_id: user.contact.id) ? true : false
  end
end

For Rails 5, use left_outer_joins:

comments = Comment.includes(:read_receipts).left_outer_joins(:read_receipts)
comments.map do |c|
  c.read_receipt(some_user)
end

Upvotes: 3

Related Questions