Well, its not exactly a secret. It sure isn’t well documented however. Recently, I wanted to return a query that spanned multiple database tables. I decided to go with find_by_sql because of the mind-blowing idiocy with which this legacy database was structured. I will take a watered down version of what I was attempting to do to demonstrate how we can expose some “hidden” functionality of ActiveRecord’s find_by_sql method.
Channel table: ------------------------------------ id | title | description | user_id ------------------------------------ 1 | first | the first channel | 1 User table: ----------- id | name ----------- 1 | ben
After I constructed my find_by_sql query, it looked something like this:
Channel.find_by_sql("SELECT a.*, b.name FROM channel a, user b WHERE a.user_id = b.id")
This query selects all columns from table a (channel), and a single column from table b (user). This is pretty standard, as many queries need to gather values from multiple table columns in a single SELECT operation.
Running this query, you will receive an array of Channel instances with all the attributes filled in for the channel model. Missing however, will be the attributes from any table other than “Channel”:
Channel.find_by_sql("SELECT a.*, b.name FROM channel a, user b WHERE a.user_id = b.id") => "[#]"
Notice how the “name” column from table b (user) is not present in the display? You can even query this attribute directly:
c = Channel.find_by_sql("SELECT a.*, b.name FROM channel a, user b WHERE a.user_id = b.id") => "[#]" c[0].name => NoMethodError: undefined method 'name' for #
We could create an attr_accessor for the Channel class, and this would resolve the NoMethodError, but it still won’t be populated for our Channel instance after a find_by_sql.
After some digging around in the source code, and online, I came across this posting, which made the brilliant suggestion of looking in channel.attributes. This method will list an array of attributes that ActiveRecord knows about. Take a look at channel.attributes.keys:
c.attributes.keys => ["id", "title", "description", "user_id", "name"]
There it is! Our “missing” name attribute from the SELECT query has been located. Accessing the value for this attribute is trivial:
c.attributes["name"] => "ben"
We can do this with as many “extra” columns as we want. If two column names conflict (say channels had a column “name”, and users also had a column “name”), the database will return “name”, and “name_1” respectively. This is a really powerful feature of ActiveRecord that will encourage people to stick with the ORM, since they can still write SQL in a pinch.
Bonus: Customizing .to_json to include find_by_sql attributes
In the preceding example, the attribute “name” would not be included in the output of a “.to_json” call, as in the following example:
c.attributes.keys => ["id", "title", "description", "user_id", "name"] c.to_json => "{"channel":{"id":1,"title":"first","description":"The first channel"}}"
This is where we can customize what is included in our JSON output. This article showed me that you can use the :methods argument with to_json to explicitly include any custom attributes, such as those that are attr_accessor objects in your class. When passing in the :methods argument, I must specify which attributes to include:
c.attributes.keys c.to_json => "{"channel":{"id":1,"title":"first","description":"The first channel"}}" c.to_json :methods => :name => "{"channel":{"id":1,"title":"first","description":"The first channel","name":"ben"}}"
Good job Rails team! No ugly hacks, or overrides needed today.