MetaUpdates

Comments

MetaSearch and MetaWhere updates, I mean. I spent some time over the weekend (and technically tonight, as well) wrapping up some nifty new features on MetaSearch and MetaWhere. Read on for the highlights.

MetaSearch

It's been a few weeks since my last update on MetaSearch, but that doesn't mean there's been no new progress. Hot on the heels of being featured in a talk by Tse-Ching Ho at Rubyconf Taiwan, a new release of MetaSearch incorporates the following changes.

Checkboxy changes

First, I've updated check_boxes and collection_check_boxes to return an array of MetaSearch::Checks when not passed a block. This lets you take advantage of methods like in_groups_of, for better formatting of your check boxes:

      <h2>How many heads?</h2>
      <p>
        <% f.check_boxes(:number_of_heads_in,
           [['One', 1], ['Two', 2], ['Three', 3]],
           :class => 'checkboxy').in_groups_of(2, false) do |checks| %>
          <% checks.each do |check| %>
            <%= check.box %>
            <%= check.label %>
          <% end %>
          <br />
        <% end %>
      </p>

Saving your fingers from pain

metasearch_exclude_attr and metasearch_exclude_assoc will still work, for the time being, to exclude attributes and associations from searches, but you'll hopefully appreciate their less underscore-y replacements, inspired by attr_protected: attr_unsearchable and assoc_unsearchable. More importantly, inspired by attr_accessible, we now have attr_searchable and assoc_searchable, for those of you who prefer whitelists.

search_methods

Previously, the only way to add additional search types to MetaSearch was to add a new Where. Now, you can also expose certain class methods as searches. The only rule is that the method must return an ActiveRecord::Relation so that MetaSearch can continue to extend the search with other attributes. Conveniently, scopes (formerly "named scopes") do this already. Want to search on backwards names or find overpaid slackers? Here ya go:

      class Company < ActiveRecord::Base
        has_many :slackers, :class_name => "Developer", :conditions => {:slacker => true}
        scope :backwards_name, lambda {|name| where(:name => name.reverse)}
        scope :with_slackers_by_name_and_salary_range,
          lambda {|name, low, high|
            joins(:slackers).where(:developers => {:name => name, :salary => low..high})
          }

        search_methods :backwards_name
        search_methods :with_slackers_by_name_and_salary_range,
          :splat_param => true, :type => [:string, :integer, :integer]
      end

Head on over to the docs for the full scoop.

MetaWhere

Since MetaWhere's introduction, I've been busy tweaking it to make it more helpful. Here's what I've come up with.

More finger-saving goodness

Using :column[:method] to access predicate methods required too much typing. Now, you can do it with :column.method:

      Article.where(
        :title.matches % 'Hello%' &
        {:created_at.lt => Time.now, :created_at.gt => 1.year.ago}
      ).to_sql
      => SELECT "articles".* FROM  "articles"
         WHERE (("articles"."title" LIKE 'Hello%' AND
         ("articles"."created_at" < '2010-04-16 01:04:38.023615' AND
          "articles"."created_at" > '2009-04-16 01:04:38.023720')))

Autojoin

Normally, you have to be sure to join (or include, which will join if conditions warrant) any associations that you’re including in your wheres. With the latest version of MetaWhere, however, you can just build up your relation’s conditions, and tack an autojoin anywhere in the chain. MetaWhere will check out the associations you’re using in your conditions and join them automatically (if they aren’t already joined).

    Article.where(:comments => [:body.like % '%FIRST POST%']).autojoin.to_sql
    => SELECT "articles".* FROM "articles"
       INNER JOIN "comments" ON "comments"."article_id" = "articles"."id"
       WHERE (("comments"."body" LIKE '%FIRST POST%'))

Remember: joins will return duplicate rows if your conditions don’t prevent it, so you might want to tack on a uniq as well.

Intelligent hash condition mapping

Yeah, long name. I don't know what else to call it though.

This is one of those things I hope you find so intuitive that you forget it wasn’t built in already.

PredicateBuilder (the part of ActiveRecord responsible for turning your conditions hash into a valid SQL query) will allow you to nest conditions in order to specify a table that the conditions apply to:

      Article.joins(:comments).where(:comments => {:body => 'hey'}).to_sql
      => SELECT "articles".* FROM "articles" INNER JOIN "comments"
         ON "comments"."article_id" = "articles"."id"
         WHERE ("comments"."body" = 'hey')

This feels pretty magical at first, but the magic quickly breaks down. Consider an association named :other_comments that is just a condition against comments:

      Article.joins(:other_comments).where(
        :other_comments => {:body => 'hey'}
      ).to_sql
      => ActiveRecord::StatementInvalid: No attribute named `body`
         exists for table `other_comments`

Ick. This is because the query is being created against tables, and not against associations. You’d need to do…

      Article.joins(:other_comments).where(:comments => {:body => 'hey'})

...instead. But not with MetaWhere:

      Article.joins(:other_comments).where(
        :other_comments => {:body => 'hey'}
      ).to_sql
      => SELECT "articles".* FROM "articles"
         INNER JOIN "comments" ON "comments"."article_id" = "articles"."id"
         WHERE (("comments"."body" = 'hey'))

Of course, it’s even simpler with autojoin, but the general idea is that if an association with the name provided exists, MetaWhere::PredicateBuilder will build the conditions against that association’s table, before falling back to a standard table name scheme. It also handles nested associations:

      Article.where(
        :comments => {
          :body => 'yo',
          :moderations => [:value < 0]
        },
        :other_comments => {:body => 'hey'}
      ).autojoin.to_sql
      => SELECT "articles".* FROM "articles"
         INNER JOIN "comments" ON "comments"."article_id" = "articles"."id"
         INNER JOIN "moderations" ON "moderations"."comment_id" = "comments"."id"
         INNER JOIN "comments" "other_comments_articles"
           ON "other_comments_articles"."article_id" = "articles"."id"
        WHERE (("comments"."body" = 'yo' AND "moderations"."value" < 0
          AND "other_comments_articles"."body" = 'hey'))

I'll admit this is a contrived example, but I hope it illustrates the feature all the same. I'm sure you'll find some uses for this. For instance, it lets you dynamically build up a conditions hash traversing your model associations, without worrying about what the eventual table aliases will be. MetaWhere will work all of that out for you.

That's it for now. Let me know what you think, either in this post, or on Twitter!

comments powered by Disqus