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 Task
s in that Project
instance?
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.