rlucas.net: The Next Generation Rotating Header Image

Ruby’s ActiveRecord Makes Dropping to Raw SQL a Royal Pain (Probably on Purpose)

The opinionated programmers behind Rails have generally done a good job. (There are couple of FUBARs in their bag of tricks, such as the boneheaded choice to use pluralized table names (in some places) and use automagical pluralization code to try and mediate between the singular and plural.)

There’s another item I’d like to bring up, however, and that’s the fact that ActiveRecord intentionally cripples your ability to do raw SQL queries. This is, I’m sure, done to discourage raw SQL hacking in favor of using the ActiveRecord objects (which, for small numbers of objects, is admittedly a superior way to do many things, because of concerns for clarity, maintainability, etc.).

However, sometimes you need SQL, dammit. Especially when you’re doing a correlation between, say, different tags that describe business plans, and the people that link those business plans together, plus the number of times that such tags appear, there’s just no sense in pulling thousands of records into memory, instantiating Ruby objects, and improperly reimplementing basic CS sorting algorithms to link them up. You’ve got all that sitting right there in your RDBMS.

ActiveRecord lets you do something like this:

BusinessPlan.find_by_sql(    [      'SELECT s2.id FROM (COMPLICATED_SUBSELECT) AS s2',      var1, var2, var3    ] )  

Which will run the complicated SQL and replace the bind vars (question marks) in the raw SQL with var1, var2, var3, etc., and give you a bunch of BusinessPlan objects that it’s instantiated off those IDs. Easy enough.

But what if you need not merely to get the objects, but to get some other important info (say, COUNT(something)) out? You’re shit out of luck with ActiveRecord. The .connection.select_all method returns you an array of record hashes, but it requires fully-baked SQL (no bind vars).

  • You could manually construct the SQL and manually quote each bind variable into its place, but avoiding that kind of retarded scut work is exactly why you’re using Rails in the first place.
  • You could try and get the DBI handle that underlies ActiveRecord (does it?), but it’s very unclear as to how or if you can do that. If you call .connection.raw_connection you get a PGConn object (for PostgreSQL), not a DBI handle.
  • You could open up your own new DBI handle, which involves recapitulating the Rails initialization code for ripping the config values and rewriting connection-pooling code, which is bad for all sorts of reasons, not least of which is that you’re already f’ing connected to the DB!

WTF? If you read the code for the find_by_sql method, you’ll see:

def find_by_sql(sql)   connection.select_all(     sanitize_sql(sql),      "#{name} Load"   ).collect! { |record| instantiate(record) } end  

Given this, you might think: “aha, I’ll just use a similar method and pass to sanitize_sql an array with my SQL and bind vars, then pass that on to select_all. No can do. sanitize_sql is a protected method.

So, here’s my encapsulation-breaking, OO-unfriendly, scofflaw workaround to let you have access to what you should already get: a decent bit of code for binding SQL parameters:

(In helpers/application_helper.rb)

arb = ActiveRecord::Base def arb.sanitize_fucking_sql(*args)   sanitize_sql(*args) end  

Now, you can happily go about your business and, when necessary, call ActiveRecord.sanitize_fucking_sql(...) to get ‘er done. No special-purpose DB connections, no wrangling thousands of objects in memory.

Caveats:

  1. I suck at Ruby, I know. There’s a more elegant way to add a public method to a class, but that works and I understand it.
  2. Eventually, this will break. But it will probably be a long time and the, er, unique method signature I suggest should be easily globally replaced.

6 Comments

  1. xc says:

    Thanks! I was looking for a way to do “?” queries like find_by_sql does with connection.select_all and this resolved it.

  2. xc says:

    Also apparently sanitize_sql is deprecated, sanitize_sql_array works instead.

    http://apidock.com/rails/ActiveRecord/Base/sanitize_sql/class

  3. Alfredo Rico says:

    Hi friend!.. I think there is a better way to do what you meant..

    x = ActiveRecord::Base.connection.select_all( ActiveRecord::Base.send(“sanitize_sql_array”,[“select foo from bar where name like = ?, var] ) )

    Done!…

  4. Alfredo Rico says:

    class ActiveRecord::Base
    def self.select_all_sanitizado(arreglo)
    connection.select_all(sanitize_sql_array(arreglo))
    end
    end

  5. taelor says:

    ActiveRecord::Base.connection.execute(“select count(*) from your_table_or_complicated_thing”).fetch_row[0]

    I mean, your not “shit out of luck with ActiveRecord” right? Its even all in one line for you (not counting the sql of course).

  6. rlucas says:

    Taelor, read the post again. It’s not about how to ship a plain string down the wire (duh) but how to get parameterized / placeholder-friendly queries with automatic escaping.

Leave a Reply

Your email address will not be published. Required fields are marked *