I started reading ActiveRecord::Base a few days ago and found 8 things that I didn’t know about that it offered. I also only made it about 1/4 of the way through the code. Here are a few new things I’ve learned upon further reading:
1. Find by multiple ids
Not only can you find a single record by calling find_by_id, you can find multiple records by providing an array of ids.
User.find_by_id([1, 12, 55]) # Returns 3 User objects with ids of 1, 12, and 55. If any isn't found, then RecordNotFound is raised.
2. Locking database records
If you have multiple processes that may update the same record (like incrementing a counter), then you may run into a problem where they both pull the record when the counter = 42. They each update the counter to 43 and save the record. This results in a deviation from reality of 1.
The solution is to lock the record while updating it. Here’s the code:
User.transaction do user = User.find(1, :lock => true) user.counter += 1 user.save! end
If second process running the above code executes while there is a lock in place, it will block until the first process finishes its transaction, at which point, the lock is lifted. The second can then get the record, now updated, and do its update.
3. Object.allocate
When doing any sort of find on an ActiveRecord model, after the query has been executed, the rows of the results are instantiated into objects by the instantiate method:
(From ActiveRecord 2.3.4)
def instantiate(record) object = if subclass_name = record[inheritance_column] # No type given. if subclass_name.empty? allocate else # Ignore type if no column is present since it was probably # pulled in from a sloppy join. unless columns_hash.include?(inheritance_column) allocate else begin compute_type(subclass_name).allocate rescue NameError raise SubclassNotFound, "The single-table inheritance mechanism failed to locate the subclass: '#{record[inheritance_column]}'. " + "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + "Please rename this column if you didn't intend it to be used for storing the inheritance class " + "or overwrite #{self.to_s}.inheritance_column to use another column for that information." end end end else allocate end object.instance_variable_set("@attributes", record) object.instance_variable_set("@attributes_cache", Hash.new) if object.respond_to_without_attributes?(:after_find) object.send(:callback, :after_find) end if object.respond_to_without_attributes?(:after_initialize) object.send(:callback, :after_initialize) end object end
Unless you’re using Single Table Inheritance, the only thing that is interesting is the allocate on line 1646 and the code following it.
Effectively, from what I can gather, Class#allocate creates the space in memory for the object. The following lines set the attributes for the ActiveRecord object and then trigger the callbacks.
According to this article the purpose of using allocate, rather than new is to avoid calling the ActiveRecord initialize method, which sets things up like ActiveRecord::Base#new_record? returning true when the record is not new.
I’m still learning about this stuff, so if you have anything to add, I’d appreciate it. (I may start reading the C code behind Ruby itself next.)
4. Attribute Level Access Control
I’ve seen this before, but I didn’t realize that there were different approaches to the access level on your attributes. Rails gives you three settings for controlling mass assignment on your Model attributes:
- attr_protected
- attr_accessible
- attr_readonly
They’re pretty self explanatory. If you declare any attribute protected, it opens all other attributes that are not declared protected for mass assignment. I’m sure you can understand the “always open” security setting’s implication.
If you declare an attribute accessible, all other non-accessible attributes are protected from mass assignment. Having been in IT and being security minded, this seems like the idea way to go.
Finally, I’m having trouble thinking of an instance where you might make an attribute readonly. It may make sense to block changes to created_at or created_by attributes, but besides those, prohibiting all updates seems a little excessive in most cases.