Ernie Miller

No, I don't work in NYC, DC, or the valley, and I'm cool with that.

RSS Feed

A Belated Post on Squeel Sifters

Posted by Ernie on September 29, 2011 at 8:15 am

A few weeks ago, I added an interesting feature to Squeel, and never really said much about it other than a single tweet and a brief reference in the README. I’m calling the feature “sifters”, and I wanted to write a little bit about the purpose they serve.

Scopes Fall Short

We can all generally agree on the principle of DRY, or “Don’t Repeat Yourself” when it comes to writing software. In the realm of ActiveRecord, we have a really useful tool in our arsenal to enhance readability and enable easy reuse of query logic. That tool is scope, and it needs no introduction.

In the case of adding conditions to a WHERE clause, scopes work great, so long as you want to add those conditions against the base model being used in your query. We can just write something like:

class User < ActiveRecord::Base
  scope :name_starts_or_ends_with, 
        lambda {|str| where{(name =~ "#{str}%") | (name =~ "%#{str}")}}
end
 
User.name_starts_or_ends_with('bob')
# => SELECT "users".* FROM "users"  
     WHERE (("users"."name" LIKE 'bob%' OR "users"."name" LIKE '%bob'))

This works great, for that limited case. But what if we would really like to use that condition against users, but users are part of a join? Scopes fail us, then.

Sure, we could write a scope on, say, an Article class…

class Article < ActiveRecord::Base
  scope :authored_by_users_with_name_starting_or_ending_with,
        lambda {|str| joins(:user).
                      where{user => ((name =~ "#{str}%") | (name =~ "%#{str}"))}
end

…but that’s a bad idea for a few reasons, the most problematic being that now we’re making assumptions about a User’s column names from the article class. There’s also the problem of needing a similar method on any other class we might like to filter based on user names.

It would be great if we had a way to:

  1. Write a reusable bundle of conditions against a model’s attributes in the code of the model it pertains to
  2. Use these condition bundles even through an association
  3. Abstract away the implementation details of the condition

Sifters Fill the Gap

That’s exactly what sifters are designed to solve. A sifter is defined similarly to a scope:

# Define a sifter via a class macro...
sifter :name_starts_or_ends_with do |str|
  (name =~ "#{str}%") | (name =~ "%#{str}")
end
# ...or a class method...
def self.name_starts_or_ends_with(str)
  squeel{(name =~ "#{str}%") | (name =~ "%#{str}")}
end

You can then use the sifter by calling sift within the Squeel DSL block, with the sifter name and its parameters (this example used only one, but you can write sifters that take as many as you like):

User.where{sift :name_starts_or_ends_with, 'bob'}
# => SELECT "users".* FROM "users"  
     WHERE (("users"."name" LIKE 'bob%' OR "users"."name" LIKE '%bob'))
 
Article.joins(:user).where{user.sift :name_starts_or_ends_with, 'bob'}
# => SELECT "articles".* FROM "articles" 
     INNER JOIN "users" ON "users"."id" = "articles"."person_id" 
     WHERE (("users"."name" LIKE 'bob%' OR "users"."name" LIKE '%bob'))

That’s about it. I hope you find sifters useful!

Filed under Blog
Tagged as , ,
You can leave a comment, or trackback from your own site.
  • Peter Hamilton

    I like the feature, but how do I nest sifters deeper?

    Say, if I wanted to find all comments on articles written by a certain user.

    I would like to set a sifter on articles that calls the sifter in users.  Something like (in Article):

    sifter :by_users_with_matching_names do |string|  joins(:user).where{user.sift :name_starts_or_ends_with, string}endThen do Comment.joins(:article).where{article.sift :by_users_with_matching_names, “Bob”}

    That doesn’t work, but I feel like it should be possible.  Is there any good way to do it?

About

I'm Ernie Miller. But then, you probably knew that by looking at the page title, or the URL. I'm a Ruby programmer in Louisville, Kentucky. This blog used to be called "metautonomo.us", which I thought was kind of clever, but nobody, including me, could type it. Lesson learned.