exAspArk / batch-loader

:zap: Powerful tool for avoiding N+1 DB or HTTP queries
https://engineering.universe.com/batching-a-powerful-way-to-solve-n-1-queries-every-rubyist-should-know-24e20c6e7b94
MIT License
1.04k stars 52 forks source link

Will generate multiple queries #25

Closed maletor closed 5 years ago

maletor commented 6 years ago

If you have a Post with many Comments and you also want to query individual Comments outside of a given Post, it's not possible to batch all of those Comments together.

I'm running into a situation where I need a universal "cache" of objects; it's not enough to just nest them under one association.


Different example from a MessageType object with Users and Receipts (join table for users and messages).

BatchLoader.for(obj.sender_id).batch do |ids, loader|
  User.where(id: ids).each { |record| loader.call(record.id, record) }
end

BatchLoader.for(obj.id).batch(default_value: []) do |message_ids, loader|
  receipts = Receipt.where(message_id: message_ids).includes(:user)
  receipts.each { |r| loader.call(r.message_id) { |memo| memo << r.user } }
end         

How do you make it so that when a sender is also a recipient, it's not broken into a separate query? Can you nest BatchLoaders? I wasn't able to get any success with that.

exAspArk commented 6 years ago

Hey, @maletor.

Can you nest BatchLoaders?

Yes, it should be possible. Here is some code example https://github.com/exAspArk/batch-loader/issues/23#issuecomment-417729982. And here is a spec for that:

https://github.com/exAspArk/batch-loader/blob/5394d70970b55a090f7289c8f23ca2a642833324/spec/batch_loader_spec.rb#L66-L87

Different example from a MessageType object with Users and Receipts (join table for users and messages).

I don't fully understand your code example, will need a bit more information about the context: what obj instances are, which BatchLoader belongs to which type, what are the relations in DB.

maletor commented 6 years ago

Hey @exAspArk,

If you have two objects you want to cache is there a way to prime like with facebook dataloader?

maletor commented 6 years ago

If you have a has_many through relationship so in order to map the entities you have to go through a middle relation, a join table. How do you later access that join table query?

BatchLoader.for(message.id).batch(default_value: []) do |ids, loader|
  receipts = MessageParticipant.where(message_id: ids)
  receipts = Pundit.policy_scope!(auth_context, receipts)
  receipts.each do |r| 
    loader.call(r.notification_id) do |memo| 
    memo << ::UserGenerator.by_id(auth_context, r.receiver_id)
  end
end

I can't come back to receipts now. It's just lost. Can I add it to another loader in the call block? I tried to unsuccessfully. Suppose later I want to be able to do get the receipts by message id but instead of returning users return receipts, the join table.

exAspArk commented 5 years ago

Hey @maletor,

It depends on your specific situation. If you'd like to "cache" receipts and use them in different loaders, you could try something like:

def cached_receipts(ids)
  BatchLoader.for(message_ids).batch do |ids_ids, loader|
    ids_ids.each do |ids|
      receipts = MessageParticipant.where(message_id: ids)
      receipts = Pundit.policy_scope!(auth_context, receipts)
      loader.call(ids, receipts)
    end
  end
end

BatchLoader.for(message.id).batch(default_value: []) do |ids, loader|
  cached_receipts(ids).each { |r| ... }
end

BatchLoader.for(message.id).batch(default_value: []) do |ids, loader|
  cached_receipts(ids).each { |r| ... }
end

Going to close the issue.