As a little follow up to the gotchas, I updated Squirrel. Its pagination is compatible with the latest will_paginate now. However, more important is the fact that you can use Squirrel blocks to build scopes:
User.scoped{ name.not =~ "Jon%" }.ordered.find(:all)
It works just like scoped
does normally, but it lets you give Squirrel-style
blocks. This lets you keep all your nice named_scope
methods and it lets you
do all the crazy crap that Squirrel makes look so much nicer at the same time.
A Refresher
For those of you who may not have seen Squirrel before, it’s a plugin for ActiveRecord that allows for a more Ruby-ish syntax when specifying queries. It’s especially handy for advanced searches and the like, where lots of joins and conditionals are required. It makes those a lot more easy to read, and makes absolutely sure that it uses the right names to refer to the right columns, which isn’t always as straightforward as you might think on a complex join.
Sadly, we can’t actually use named_scope
with a Squirrel block; it’s already
using blocks for other things. However, we can do the next best thing, which is
creating class methods on our models. Since named_scopes proxy all their methods
back to where they were created, everything will work out fine.
Let’s say we had an app that lets users put up postings for a length of time. We can also tag those postings. We want to allow people to search for the postings, so we whip up a Squirrel query like so:
class Post
named_scope :ordered, :order => "created_on DESC"
named_scope :limit, lambda{|x| {:limit => x} }
named_scope :after, lambda{|d| {
:conditions => ["posts.created_on > ?", d]
} }
def self.search(params)
scoped do
any do
title.contains? params[:keyword]
body.contains? params[:keyword]
end
expired == false unless params[:all] == "1"
created_on > params[:timeframe].to_i.days.ago
params[:tag].split(",").each do |tag|
tags.name == tag.strip
end
end
end
end
This lets us chain search
alongside the limit
and ordered
scopes (using
some params
that look like they came in a Rails request):
Post.search({:all => "1",
:tag => "one, two",
:keyword => "Something",
:timeframe => ""}).ordered.limit(5)
This performs the following query:
SELECT DISTINCT "posts".id FROM "posts"
LEFT OUTER JOIN "tags" ON tags.post_id = posts.id
WHERE ((posts.created_on > '2008-06-25 15::36'
AND (posts.title LIKE '%Something%' OR posts.body LIKE '%Something%')
AND (tags.name = 'one') AND (tags.name = 'two')))
ORDER BY created_on DESC
LIMIT 5
Now let’s say those Posts have Comments (and both of those belong_to :user
).
We can find all Posts that have Comments by their author like so:
def Post.with_author_comments
scoped { user.id == comments.user.id }
end
So to find all the Posts in the last week that have a comment by their Author, we can say:
Post.with_author_comments.after(7.days.ago)
Which gets us this lovely bit of SQL:
SELECT
... 32 aliases snipped ...
FROM "posts"
LEFT OUTER JOIN "comments" ON comments.post_id = posts.id
LEFT OUTER JOIN "users" ON "users".id = "comments".user_id
LEFT OUTER JOIN "users" users_posts ON "users_posts".id = "posts".user_id
WHERE ((posts.created_at > '2008-06-18 15:35:27')
AND (((users_posts.id = users.id))))
And we can even find all the Users who have never made a Comment this way:
def User.without_comments
scoped { comments.id.nil? }
end
Each of these works exactly like any other namedscope and can be used in any situation where a namedscope could also be used.
Update: After I hit publish on this, I noticed that the Squirrel-style named scopes need to be specified first in the chain or they clobber what came before, but otherwise work fine. Using blocks with scoped not using the named methods also works completely fine. I’ll be looking into why this is and I’ll have an update soon. So, you can use them almost exactly the same as regular named scopes, anyway.
You can get the latest on Squirrel’s github page.
Visit our Open Source page to learn more about our team’s contributions.