views:

151

answers:

4

Taking following association declaration as an example:

class Post
 has_many :comments
end

Just by declaring the has_many :comments, ActiveRecord adds several methods of which I am particularly interested in comments which returns array of comments. I browsed through the code and following seems to be the callback sequence:

def has_many(association_id, options = {}, &extension)
  reflection = create_has_many_reflection(association_id, options, &extension)
  configure_dependency_for_has_many(reflection)
  add_association_callbacks(reflection.name, reflection.options)

  if options[:through]
    collection_accessor_methods(reflection, HasManyThroughAssociation)
  else
    collection_accessor_methods(reflection, HasManyAssociation)
  end
end

def collection_accessor_methods(reflection, association_proxy_class, writer = true)
  collection_reader_method(reflection, association_proxy_class)

  if writer
    define_method("#{reflection.name}=") do |new_value|
      # Loads proxy class instance (defined in collection_reader_method) if not already loaded
      association = send(reflection.name)
      association.replace(new_value)
      association
    end

    define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
      ids = (new_value || []).reject { |nid| nid.blank? }
      send("#{reflection.name}=", reflection.class_name.constantize.find(ids))
    end
  end
end

def collection_reader_method(reflection, association_proxy_class)
  define_method(reflection.name) do |*params|
    force_reload = params.first unless params.empty?
    association = association_instance_get(reflection.name)

    unless association
      association = association_proxy_class.new(self, reflection)
      association_instance_set(reflection.name, association)
    end

    association.reload if force_reload

    association
  end

  define_method("#{reflection.name.to_s.singularize}_ids") do
    if send(reflection.name).loaded? || reflection.options[:finder_sql]
      send(reflection.name).map(&:id)
    else
      send(reflection.name).all(:select => "#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map(&:id)
    end
  end
end

In this sequence of callbacks, where exactly is the actual SQL being executed for retrieving the comments when I do @post.comments ?

A: 

Here you are: a standard AR query getting all the ids of the associated objects

send(reflection.name).all(:select => "#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map(&:id)

but sure Activerecord is messy... a re-implementation (better without eval) of has_many maybe can be useful for you:

def has_many(children)
  send(:define_method, children){ eval(children.to_s.singularize.capitalize).all( :conditions => { self.class.name.downcase => name }) }
end
makevoid
I am not trying to re-implement what AR already does. What I am trying to figure out is when you say @post.comments where exactly is the SQL related to fetching the comments is implemented (which file and which method). By looking at the collection_reader_method, I dont see anywhere it calls the SQL, I even dug deeper in the association_proxy.rb too
satynos
A: 

In the association reader the line

association = association_proxy_class.new(self, reflection)

in the end will be responsible for executing the find, when the instance variable is "asked" for and "sees" that @loaded is false.

@adamaig: From the code I have pasted, I can easily infer that association = association_proxy_class.new(self, reflection) is the method that is responsible, but I want to know how the execution traverses when @post.comments is called. From what I see in the AR code, association_proxy_class.new(self, reflection) is nothing but HasManyAssociation.new(@post, reflection) and as AR is defining the method comments and reviewing the method body, I can't see where exactly AR is calling the SQL.
satynos
A: 

You need to dig deeper into the definition of HasManyAssociation.

colletion_reader_method defines a method called comments on your Post class. When the comments method is called, it ensures there's a proxy object of class HasManyAssociation stored away (you'll need to dig into the association_instance_set method to see where exactly it stores it), it then returns this proxy object.

I presume the SQL comes in when you call a method on the proxy, for example, calling each, all or accessing an index with [].

fd
I looked deep into the association_instance_set and association_instance_get, and I still didn't see where AR is actually executing/calling the SQL to load the comments of a particular post when we call @post.comments
satynos
I don't think it does just for calling @post.comments, unless maybe it does this in the constructor for HasManyAssociation. As I said association_instance_(get/set) is not where the SQL will come from, it just stores the proxy object in your model. Looking at HasManyAssociation should be the route to follow.
fd
Had a quick look at the source, the database directives are split into two major places depending on whether they are DB specific or not (also whether they are queries or DB management directives). Database specific and database management SQL/directives are in the connection adapters (either the abstract adapter and related classes, or in the adapter specific to the DB, for example the mysql adapter). The rest is in the activerecord base class, and the associations class although these get called from the association proxy classes.
fd
@fd: Thanks for taking time to explain the inner workings. I did understand how AR works on the AssociationProxy (in this case HasManyAssociation < AssociationCollection < AssociationProxy) so when you say @post.comments.count or @post.comments.find, internally it is calling load_target, but while we say @post.comments I don't see how and where it is calling load_target which has to be called somewhere to use the constructed SQL that was constructed in AssociationCollection#initialize with a call to the method construct_sql
satynos
My understanding is that @post.comments doesn't return any results from the DB, just the proxy object, it doesn't need to construct the SQL or hit the DB as far as I can tell. Of course, if you run @post.comments in IRB then it calls something like to_s or inspect on the proxy object which would go off and run some SQL. Not 100% certain about this, think of this as a suggestion of what you might be after. Let me know if it helps.
fd
A: 

I am not 100% sure I understand what you are looking for.

The sql generation is not in one place in AR. Some of the database specific things are in the database "connection_adapters".

If you are looking for the way how the records are found in the database, look at the methods "construct_finder_sql" and "add_joins" in the ActiveRecord::Base module.

    def construct_finder_sql(options)
      scope = scope(:find)
      sql  = "SELECT #{options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))} "
      sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "

      add_joins!(sql, options[:joins], scope)
      ...

and

    def add_joins!(sql, joins, scope = :auto)
      scope = scope(:find) if :auto == scope
      merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins])
      case merged_joins
      when Symbol, Hash, Array
        if array_of_strings?(merged_joins)
          sql << merged_joins.join(' ') + " "
        else
          join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, merged_joins, nil)
          sql << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
        end
      when String
        sql << " #{merged_joins} "
      end
    end

I hope this helps!

txwikinger