Search code examples
ruby-on-railsrubyrailscastsclass-method

Calling a class method through an object instance


Railscasts #4 uses this sample code:

class Task < ActiveRecord::Base
  belongs_to :project

  def self.find_incomplete
    find_all_by_complete(:false, :order => "created_at DESC")
  end
end

class ProjectsController < ApplicationController
  def show
    @project = Project.find(params[:id])
    @tasks = @project.tasks.find_incomplete
  end
end

Using @project.tasks.find_incomplete, only finds incomplete orders that belong to that specific Project instance.

I would expect that call to be equivalent to Task.find_incomplete, but it is not. How can that be? How does Rails (or Ruby) now to just invoke that method for those specific Tasks in that Project instance?


Solution

  • This works because scopes of ActiveRecord relations are merged. It's not that find_incomplete is running on individual task instances.

    @project.tasks creates an ActiveRecord scope of the tasks for that project instance and then that scope is still in effect when your find_incomplete method is called.

    Take a look at the documentation here: http://guides.rubyonrails.org/active_record_querying.html#scopes

    Your find_incomplete method works in the same way as the self.published example in the docs.

    Think of the underlying SQL query that would run:

    @project.tasks would create a where condition like SELECT * FROM projects WHERE project_id = <project_id>

    find_all_by_complete then merges in an and condition for complete = 0


    I think the other piece of the puzzle that might help is that @project.tasks is not just a simple array array of Task objects, although it will have been converted to such if you type project.tasks in the Rails console. project.tasks is actually an Active Record relation object (or more precisely a proxy) There are a number of reasons and benefits for this but the main 2 are that it allows chaining and it allows the underlying query to be run on demand only if/when needed.

    The relation has a sequence of rules for how method calls are delegated, one of which is to call class methods on the class of the associated objects. (the relation knows that it's a relation to Tasks)

    So you are correct when you wrote tasks.find_incomplete is still equal to Task.find_incomplete except that when find_incomplete is called through project.tasks a scope narrowing down to the project_id is already in effect.