Ernie Miller

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

RSS Feed

Why MetaWhere 0.9.2 raises errors (and why you’ll thank me later)

Posted by Ernie on August 26, 2010 at 7:00 am

If you updated MetaWhere recently, and your application started raising MetaWhereInAssociationErrors, I’m sorry. But not too sorry. Because that error probably saved you from running into strange, confusing problems later. Let me explain.

The bug hunt

If you’re a typical MetaWhere user, you’ve gotten into the habit of specifying your conditions with the improved syntax pretty quickly, and they are almost second nature to you now. They certainly were to me, at least.

So much so, in fact, that in my last project, without thinking, I had declared this in an association:

has_many :optional_photos, :class_name => "Photo",
                           :conditions => {:subtype.matches => 'optional%'}

This was a silly mistake on my part, but I’d gotten so used to using MetaWhere it didn’t hit me until later, when none of my optional photos were showing up. I was confused for a moment. I checked the log to see this query:

SELECT COUNT(*) AS count_id
FROM (SELECT 1 FROM `assets` WHERE (`assets`.`type` = 'Photo')
  AND (`assets`.project_id = 2 AND (`assets`.`subtype` = 'optional%')))
  AS subquery

Did you spot the problem? My intended LIKE query was being converted to an = query. This happens in activerecord/lib/active_record/reflection.rb, in Association#dependent_conditions:

def dependent_conditions(record, base_class, extra_conditions)
  dependent_conditions = []
  dependent_conditions << "#{primary_key_name} = #{record.send(name).send(:owner_quoted_id)}"
  dependent_conditions << "#{options[:as]}_type = '#{base_class.name}'" if options[:as]
  dependent_conditions << klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
  dependent_conditions << extra_conditions if extra_conditions
  dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
  dependent_conditions = dependent_conditions.gsub('@', '\@')
  dependent_conditions
end

The important part is the line that calls sanitize_sql on options[:conditions]. That method converts hashes to standard equality conditions.

Why this matters

“So, why not make sanitize_sql understand MetaWhere conditions? Isn’t that the solution?” I’m glad you (okay, I) asked. Because it’s completely reasonable for the existing behavior to occur in associations.

One of the key reasons you would choose to use an association over a plain instance method to access associated records is that they bake in some special magic that allows you to create new instances of the associated model. For instance, to build a new instance of a comment associated with the first article:

ruby-1.9.2-p0 > Article.first.comments.build(:body => 'hey!')
 => #<Comment id: nil, article_id: 1, body: "hey!", created_at: nil,
      updated_at: nil>

Let’s assume, for a moment, a has_many association that looks like this, though (note: Don’t do this. MetaWhere won’t let you now, anyway):

has_many :old_comments,
         :class_name => "Comment",
         :conditions => {:created_at.lt => "2009-01-01".to_time}

What would constitute the “correct” way to create a new record, now? To set the created_at value to 1 second before New Year’s Day, 2009? The beginning of time? It doesn’t really make any sense.

This is why MetaWhere will now check whether you are trying to use MetaWhere conditions in an association macro, and raise the error. Better to stop in your tracks before you go too far down that road. That road leads to madness.

If not an association, then what?

In my case, since I was only using that association to load optional photos, and not create them, I went with an instance method:

def optional_photos
  assets.where(:type => 'Photo', :subtype.matches => 'optional%')
end

If you weren’t intending to find only those objects associated with an instance, then you weren’t looking for an association in the first place, anyway — use a scope instead.

Of course, the error message will tell you as much:

MetaWhere::MetaWhereInAssociationError: The :lame_comments association has a MetaWhere::Column in its :conditions. If you actually needed to access conditions other than equality, then you most likely meant to set up a scope or method, instead. Associations only work with standard equality conditions, since they can be used to create records as well.

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

    Hey, first of all, thank you for your wonderful software! I recently updated to the new versions of meta_where and meta_search. Previously I had a “where” scope that i used in a meta_search function as follows…


    scope :contains_string, lambda {|str| where(:title.matches % "%#{str}%" | :body.matches % "%#{str}%" | :tags => [:name.matches % "%#{str}%"]).autojoin}

    search_methods :contains_string

    which was used in a search method to filter objects in a Post model (that uses acts_as_taggable_on for tagging). Before I could search with a string like “welcome” and it would return any post object that had that string in their title, body or tag, even if the object’s tag_list was empty.

    with the new version of meta_where that removes the autojoin method I changed my code to…


    scope :contains_string, lambda {|str| where(:title.matches % "%#{str}%" | :body.matches % "%#{str}%" | :tags => [:name.matches % "%#{str}%"]).joins(:tags)}

    search_methods :contains_string

    Now if i search for that same string it will only respond with Post objects that do not have an empty tag_list. I’m sorry for the long comment, I wasn’t sure if this was a bug or if I have my syntax wrong (I guess I’m approaching this a bit like a newbie since I was using that autojoin method before). Maybe I need to use something like thinking sphinx for a text search like this?

    Any help would be greatly appreciated.

  • http://thebalance.metautonomo.us Ernie

    Thanks for the kind words. Glad you’re enjoying both gems! I think there may be some confusion regarding the source of the problem. All autojoin does (did) is what you changed it to, anyway. The problem is that those joins are always inner joins, which require a match on both sides of the association. You could try an include to do a left join instead, but then you are also loading the associated objects which may not be what you want.

  • okr

    Thank you for your quick reply!

    It does sound like I need to use an include (left join) and then just select the unique entries it returns. I’m probably not doing this correctly (couldn’t find much info on using includes in named scopes).

    I changed my scope to…


    scope :contains_string, lambda {|str| where(:title.matches % "%#{str}%" | :body.matches % "%#{str}%" | :tags => [:name.matches % "%#{str}%"]).includes(:tags)}

    which gives me the following error…

    undefined method `to_sym’ for #;

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.