jgaskins / perpetuity

Persistence gem for Ruby objects using the Data Mapper pattern
http://jgaskins.org/blog/2012/04/20/data-mapper-vs-active-record/
MIT License
250 stars 14 forks source link

load_association! does not make sense #33

Closed jcostello closed 10 years ago

jcostello commented 10 years ago

I feel like load_association! doesnt make sense.

Yeah, sometime we want to retrieve some entity from the DB without quering all the references it has. But... we could do a lazy initialization to those references.

When i run this (for example):

user = Perpetuity[User].first
user.friends #=> [<User:#objectid @name="John">]

it should say "oh, you are trying to retrieve the friends from user, let me query them for you".

Does this make sence? Maybe is not that easy to implement

jgaskins commented 10 years ago

This probably isn't what you were hoping to hear, but I chose not to implement lazy loading very deliberately. It would be simple to implement due to how Perpetuity deserializes references, but lazy loading hides additional queries, leading to hidden performance bottlenecks all over your application.

If you take your example of user.friends and want to get the user's friends' friends (for example, to post to their social feed), in ActiveRecord that would cause N+2 queries (the user, their friends, and the friends list for every single one of those friends) unless you made a special association for friends' friends:

class User < ActiveRecord::Base
  has_many :friends, class_name: 'User'
  has_many :friends_friends, through: :friends, class_name: 'User'
end

… and then you'd have to remember to add includes(:friends_friends) every single time you wanted to query them. This is too much to think about to do the right thing. It defeats the purpose of the elegance of lazy loading.

Mapper#load_association! may have a weird name (I'd actually like it to be shorter since it's used so often), but finding your friends' friends (or any second-degree association) is at most 3 queries including the user themselves, no matter how many friends they have:

user_mapper = Perpetuity[User] # I've been memoizing this into my application_controller.rb
user = user_mapper.find(params[:id])
user_mapper.load_association! user, :friends
user_mapper.load_association! user.friends, :friends # Loads all friends' friends in a single query

This way, you see exactly how many queries you're triggering without looking at logs. There are a lot of things that are best handled behind the scenes, but my years in chasing performance bottlenecks in ActiveRecord have led me to believe that lazy association loading in an ORM is not one of them.

This is already long, but I want to throw in here that I'd love to be wrong about this. Lazy loading is pretty convenient when you're starting out. If someone can show me that my performance issues were ActiveRecord-specific and that lazy loading can be a clear win over eager loading, I'll go ahead and make this change.