I’m here at The Rails Edge Conference in Denver and will summarize some of the talks. This topic is of interest only to those interested in Ruby or Rails, so if that is you – read on. If not, please disregard.
Active Record Demystified by Marcel Molina Jr.
Rails Core Team member Marcel Molina Jr. presented a walkthrough of ActiveRecord::Base#Save ActiveRecord::Base.find as part of the “Active Record Demystified” talk.
Marcel walked through:
1. Connection Adapters
Connection adapters are built from:
connection_specification.rb creates and manages connections
Marcel pointed out that connection_specification.rb had been the big selling point of Rails because it uses the database yml file that used to be the only place configuration was required.
database_statements.rb turns the Rails commands into SQL statements.
On the adapter level, all SQL statements are written in terms of EXECUTE against the database connection. The SELECT statement is the base from which other statements such as select_all, select_one are built. INSERT, UPDATE, DELETE statements are also implemented against the database in terms of EXECUTE.
quoting.rb tells how to handle quoting based on different database vendors.
schema_definitions.rb performs column typecasting. It looks at the column definition from the database using the RDBMS’ adaptors’ reflection utilities. Based on the database column type, the value is coerced into a matching Ruby type.
Save calls an SQL INSERT statement if the record is new and SQL UPDATE if not.
Validations does occur because save is aliased to validation, validation then performs the validation and if passes performs the save.
The first option passed into
find is :first, :all, or an integer. Find first validates the find options. If the options validate,
find it then takes one of three paths depending on the value of the first argument:
when :first then find_initial(options) –
Implements first by adding the limit option to the SQL statement.
when :all then find_every(options) runs find_with_associations which leverages a JoinDependency class which then goes into the associations code. Associations code is some of the most powerful features of Rails, but also some of the “least attractive” implementation of Rails.
If there are no associations, then just constructs the sql by constructing an SQL statement out of the joins, conditions, limits, etc. Finally, it calls “find_by_sql”.
else find_from_ids(args, options) will find records by id or ids. Did you know that you can pass an array of ids to find!!
Dynamic finders (the ability to say
find_by_first_name_and_last_name) is supported via method_missing.
method_mising first checks the method name against a regular expression. That regex looks to see if method “looks like “a dynamic finder. If the expression is multiple parts it then splits off the finder using “and” as a delimeter to get a list of attribute names.
Then looks to see if all split out attribute names are columns in the table. If not, then it exits out (via ‘super’) to “no method error”.
If the attribute name is an attribute on the object, then it delegates out to the ActiveRecord::Base#Find
sqlLite, postgres, mySQL – are the only adapters maintained by Rails core team. The core team continuously tests against the open source RDBMs. Some other adapters are maintained by other contributors outside of the core team. Core team would love to have folks step forward and volunteer to maintain tests (continuous integration) for other RDBMs.
Positional parameters, option hashes, and keyword arguments
Originally, calls to find had positional arguments. After switching to option hashes, they have found code is more maintainable and extensible as well as making for an easier to use API (e.g. do not need to pass nil params to fulfill positional requirements). Marcel prefers using option hashes over positional parameters, but is looking forward to keyword arguments that are to come in Ruby 2.0.
Marcel talked about the word “Dynamic” and how he now really gets what it means. For example, did you know you can do?
def foo(first, second = first, third = second.size)
[first, second, third]
It is important to use method_missing appropriately. Most importantly: you have to chain back to the original behavior which most likely means ending your method_missing implementation with: