Passing a hash of conditions to find in rails

Anton Jenkins | April 21, 2009

I just stumbled across a neat trick in ActiveRecord which could help make your code more readable:

1
2
3
User.find(:first, :conditions => { :forename => 'Anton',
                                   :surname => 'Jenkins',
                                   :age => some_value_from_a_form })

To me this is so much more readable than:

1
2
User.find(:first, :conditions => ["forename = ? AND surname = ? AND age = ?",
                                  'Anton', 'Jenkins', some_value_from_a_form])

I know which one I prefer. I can’t believe I’ve never seen anyone using hashes for conditions before! Have I had my head in the sand?

Let me know if there are any gotchas with this technique. Maybe there is a good reason I’ve not seen it used before.

Comments

  1. Arthur Schreiber |

    Uhm, this "new" technique has been around for 3 years now... So nothing really special to see here. :)

  2. http://antonjenkins.myopenid.com |

    I never claimed it was new at all, and you can tell that from the date of the post I linked to. I've just not seen anyone adopt it before and I'm curious why not. Like I said - maybe I've had my head in the sand and it passed me by?! :oD

  3. http://antonjenkins.myopenid.com |

    I've just searched the second edition of the "Agile Web Development With Rails" book and it mentions using a hash but not in the same context as above. I would rather use the hash as a replacement for the comparatively unreadable array but they've only mentioned it for parsing in params:



    As of Rails 1.2, you can take this even further. If you pass just a hash as the
    condition, Rails generates a where clause where the hash keys are used as
    column names and the hash values the values to match. Thus, we could have
    written the previous code even more succinctly.

    1
    
    pos = Order.find(:all, :conditions => params[:order])
    

    (Be careful with this latter form of condition: it takes all the key/value pairs
    in the hash you pass in when constructing the condition.)
    Regardless of which form of placeholder you use, Active Record takes great
    care to quote and escape the values being substituted into the SQL. Use these
    forms of dynamic SQL, and Active Record will keep you safe from injection
    attacks.

  4. le_fnord |

    that's a normal ActiveRecord condition, as described in the documentation, the only difference is, the degree of sanitizing against sql-attacks

  5. http://antonjenkins.myopenid.com |

    The documentation says:

    "Conditions can either be specified as a string, array, or hash representing the WHERE-part of an SQL statement. The array form is to be used when the condition input is tainted and requires sanitization. The string form can be used for statements that don‘t involve tainted data. The hash form works much like the array form, except only equality and range is possible."

    The way I read that is hashes are still sanitized, even though it starts out by stating you should use arrays. Obviously you aren't going to be able to construct nested logic with multiple AND and OR constructs, but for simple equality it should be functionally the same as an array, I think.

  6. Arthur Schreiber |

    Yes, as long as you don't need any complex SQL logic, arrays and hashes behave exactly the same. The level of sanitization is the same.

    The main difference is readability, and the hash version wins at it hands down. :)

  7. Eric Anderson |

    I have always preferred the has method myself. Much cleaner. Unfortunately a majority of the time I need a different operator than = causing me to fall back on the array method. There are some plugins that try to fix this but never found one I really like.

  8. remi |

    I agree, I like the hash syntax if I'm passing more than a few arguments. But not just because it makes your code more readable. It makes the code easier to work with because the variables being passed in aren't tightly coupled to the order in which they are passed.

    #find with ?'s is fine with 1 or 2 arguments. once it has more, you start getting a stronger connascence of position (CoP). the variables are coupled to the position that they're passed in. by switching to a hash, you're reducing the coupling to connascence of name (CoN) which is better than CoP.

    Ever since seeing Jim Weirich's presentations at MWRC this year and LA Ruby Conf, I've been noticing *why* I prefer certain code to other code. If you're interested, check out his presentation:

    * http://mwrc2009.confreaks.com/14-mar-2009-18-10-the-building-blocks-of-modularity-jim-weirich.html
    *http://github.com/jimweirich/presentation_connascence

  9. http://antonjenkins@myopenid.com |

    @remi : Thank you for giving me a new phrase - CoP! I've had exactly the same thoughts regarding positioning versus naming when it comes to raw SQL. Take a basic INSERT statement for example:

    1
    2
    3
    4
    5
    6
    
    INSERT INTO users (surname, forename, age, height)
      SELECT some_column,
             another_column,
             this_is_age_apparently,
             tallness
      FROM another_table;
    

    I've got big issues with the SQL syntax there because you can imagine how hard it would become to maintain that statement if you are suddenly dealing with tens or even hundreds of columns (and seeing as I work on de-normalised data warehouses, this can happen!).

    The only way to add some sanity to proceedings is to add some aliases in there:

    1
    2
    3
    4
    5
    6
    
    INSERT INTO users (surname, forename, age, height)
      SELECT some_column             AS surname,
             another_column          AS forename,
             this_is_age_apparently  AS age,
             tallness                AS height
      FROM another_table;
    

    That reads much better. The aliases aren't enforced by the language so effectively they are like comments in this instance, but it's still progress.

    I personally think it wouldn't be a bad idea if SQL enforced the use of aliases on these types of inserts and threw syntax errors if the names of the aliases didn't match up with the corresponding column names. If you are going to have compile time checking (and SQL and PL/SQL are about as static as languages get) then you may as well go the whole hog and help protect the user from stupid positioning errors.

  10. pete |

    @remi, @anton:

    ActiveRecord can also use named parameters for sort of a compromise between the hash and array syntaxes:

    1
    2
    3
    
    User.find( :all, 
      :conditions => [ "created_at > :date and status = :active", 
                     { :date => 1.year.ago, :active => "ACTIVE" } ] )
    

  11. http://antonjenkins.myopenid.com |

    @pete : That's a good point, I forgot about that. That's quite a nice compromise because you've now got full control of the WHERE clause rather than just testing for equality but you've also got placeholders which is much more maintainable than a stream of ?'s. Yea, I like that much more.

  12. Giang Nguyen |

    the second one's security is higher

Post a comment


(lesstile enabled - surround code blocks with ---)