ActiveRecord
is a simple object-relational mapping (ORM) framework compared to other popular ORM frameworks, such as Hibernate in the Java world. Don’t let that fool you, though: Under its modest exterior, ActiveRecord
has some pretty advanced features. To really get the most effectiveness out of Rails development, you need to have more than a basic understanding of ActiveRecord
—things like knowing when to break out of the one-table/one-class pattern, or how to leverage Ruby modules to keep your code clean and free of duplication.
In this chapter, we wrap up this book’s comprehensive coverage of ActiveRecord
by reviewing callbacks, observers, single-table inheritance (STI), and polymorphic models. We also review a little bit of information about metaprogramming and Ruby domain-specific languages (DSLs) as they relate to ActiveRecord
.
This advanced feature of ActiveRecord
allows the savvy developer to attach behavior at a variety of different points along their model’s life cycle, such as after initialization, before database records are inserted, updated or removed, and so on.
Callbacks can do a variety of tasks, ranging from simple things such as logging and massaging of attribute values prior to validation, to complex calculations. Callbacks can halt the execution of the life-cycle process taking place. Some callbacks can even modify the behavior of the model class on the fly. We’ll cover all of those scenarios in this section, but first let’s get a taste of what a callback looks like. Check out the following silly example:
class Beethoven < ActiveRecord::Base before_destroy :last_words ... protected def last_words logger.info "Friends applaud, the comedy is over" end end
So prior to dying (ehrm, being destroy
’d), the last words of the Beethoven
class will always be logged for posterity. As we’ll see soon, there are 14 different opportunities to add behavior to your model in this fashion. Before we get to that list, let’s cover the mechanics of registering a callback.
Overall, the most common way to register a callback method is to declare it at the top of the class using a typical Rails macro-style class method. However, there’s a less verbose way to do it also. Simply implement the callback as a method in your class. In other words, I could have coded the prior example as follows:
class Beethoven < ActiveRecord::Base ... protected def before_destroy logger.info "Friends applaud, the comedy is over" end end
This is a rare case of the less-verbose solution being bad. In fact, it is almost always preferable, dare I say it is the Rails way, to use the callback macros over implementing callback methods, for the following reasons:
Macro-style callback declarations are added near the top of the class definition, making the existence of that callback more evident versus a method body later in the file.
Macro-style callbacks add callback methods to a queue. That means that more than one method can be hooked into the same slot in the life cycle. Callbacks will be invoked in the order in which they were added to the queue.
Callback methods for the same hook can be added to their queue at different levels of an inheritance hierarchy and still work—they won’t override each other the way that methods would.
Callbacks defined as methods on the model are always called last.
Now, if (and only if) your callback routine is really short,[1] you can add it by passing a block to the callback macro. We’re talking one-liners!
class Napoleon < ActiveRecord::Base before_destroy {|r| logger.info "Josephine..." } ... end
Except when you’re using a block, the access level for callback methods should always be protected or private. It should never be public, since callbacks should never be called from code outside the model.
Believe it or not, there are even more ways to implement callbacks, but we’ll cover those techniques further along in the chapter. For now, let’s look at the lists of callback hooks available.
In total, there are 14 types of callbacks you can register on your models! Twelve of them are matching before/after
callback pairs, such as before_validation
and after_validation
. (The other two, after_initialize
and after_find
, are special, and we’ll discuss them later in this section.)
This is the list of callback hooks available during a save
operation. (The list varies slightly depending on whether you’re saving a new or existing record.)
before_validation
before_validation_on_create
after_validation
after_validation_on_create
before_save
before_create
(for new records) and before_update
(for existing records)
ActiveRecord
talks to the database and actually does an INSERT
or UPDATE
after_create
(for new records) and before_update
(for existing records)
after_save
Delete
operations have their own two callbacks:
before_destroy
ActiveRecord
talks to the database and actually does a DELETE
after_destroy
is called after all attributes have been frozen (read-only)
If you return a Boolean false
(not nil)
from a callback method, ActiveRecord
halts the execution chain. No further callbacks are executed. The save
method will return false
, and save!
will raise a RecordNotSaved
error.
Keep in mind that since the last expression of a Ruby method is returned implicitly, it is a pretty common bug to write a callback that halts execution unintentionally. If you have an object with callbacks that mysteriously fails to save, make sure you aren’t returning false
by mistake.
Of course, the callback you should use for a given situation depends on what you’re trying to accomplish. The best I can do is to serve up some examples to inspire you with your own code.
The most common examples of using before_validate
callbacks have to do with cleaning up user-entered attributes. For example, the following CreditCard
class (as cited in the Rails API docs) cleans up its number
attribute so that false negatives don’t occur on validation:
class CreditCard < ActiveRecord::Base ... private def before_validation_on_create # Strip everything in the number except digits self.number = number.gsub(/[^0-9]/, "") end end
Assume that you have an application that tracks addresses and has mapping features. Addresses should always be geocoded before saving, so that they can be displayed rapidly on a map later.[2]
As is often the case, the wording of the requirement itself points you in the direction of the before_save
callback:
class Address < ActiveRecord::Base include GeoKit::Geocoders before_save :geolocate validates_presence_of :line_one, :state, :zip ... private def geolocate res = GoogleGeocoder.geocode(to_s) self.latitude = res.lat self.longitude = res.lng end end
Before we move on, there are a couple of additional considerations. The preceding code works great if the geocoding succeeds, but what if it doesn’t? Do we still want to allow the record to be saved? If not, we should halt the execution chain:
def geolocate res = GoogleGeocoder.geocode(to_s) return false if not res.success # halt execution self.latitude = res.lat self.longitude = res.lng end
The only problem remaining is that we give the rest of our code (and by extension, the end user) no indication of why the chain was halted. Even though we’re not in a validation routine, I think we can put the errors
collection to good use here:
def geolocate res = GoogleGeocoder.geocode(to_s) if res.success self.latitude = res.lat self.longitude = res.lng else errors.add_to_base("Geocoding failed. Please check address.") return false end end
If the geocoding fails, we add a base error message (for the whole object) and halt execution, so that the record is not saved.
What if your application has to handle important kinds of data that, once entered, should never be deleted? Perhaps it would make sense to hook into ActiveRecord
’s destroy mechanism and somehow mark the record as deleted instead?
The following example depends on the accounts
table having a deleted_at
datetime column.
class Account < ActiveRecord::Base ... def before_destroy update_attribute(:deleted_at, Time.now) and return false end end
I chose to implement it as a callback method so that I am guaranteed it will execute last in the before_destroy
queue. It returns false
so that execution is halted and the underlying record is not actually deleted from the database.[3]
It’s probably worth mentioning that there are ways that Rails allows you to unintentionally circumvent before_destroy
callbacks:
The delete
and delete_all
class methods of ActiveRecord::Base
are almost identical. They remove rows directly from the database without instantiating the corresponding model instances, which means no callbacks will occur.
Model objects in associations defined with the option :dependent => :delete_all
will be deleted directly from the database when removed from the collection using the association’s clear
or delete
methods.
Model objects that have files associated with them, such as attachment records and uploaded images, can clean up after themselves when deleted using the after_destroy
callback. The following method from Rick Olson’s excellent AttachmentFu[4] plugin is a good example:
# Destroys the file. Called in the after_destroy callback def destroy_file FileUtils.rm(full_filename) ... rescue logger.info "Exception destroying #{full_filename ... }" logger.warn $!.backtrace.collect { |b| " > #{b}" }.join(" ") end
The after_initialize
callback is invoked whenever a new ActiveRecord
model is instantiated (either from scratch or from the database). Having it available prevents you from having to muck around with overriding the actual initialize
method.
The after_find
callback is invoked whenever ActiveRecord
loads a model object from the database, and is actually called before after_initialize
, if both are implemented. Because after_find
and after_initialize
are called for each object found and instantiated by finders, performance constraints dictate that they can only be added as methods, and not via the callback macros.
What if you want to run some code only the first time that a model is ever instantiated, and not after each database load? There is no native callback for that scenario, but you can do it using the after_initialize
callback. Just add a condition that checks to see if it is a new record:
def after_initialize if new_record? ... end end
In a number of Rails apps that I’ve written, I’ve found it useful to capture user preferences in a serialized hash associated with the User
object. The serialize
feature of ActiveRecord
models makes this possible, since it transparently persists Ruby object graphs to a text column in the database. Unfortunately, you can’t pass it a default value, so I have to set one myself:
class User < ActiveRecord::Base serialize :preferences # defaults to nil ... private def after_initialize self.preferences ||= Hash.new end end
Using the after_initialize
callback, I can automatically populate the preferences
attribute of my user model with an empty hash, so that I never have to worry about it being nil
when I access it with code such as user.preferences [:show_help_text] = false
. Of course, you would only want to store data in serialized columns that you had no interest in querying with SQL in the future.
Ruby’s metaprogramming capabilities combined with the ability to run code whenever a model is loaded using the after_find
callback are a powerful mix. Since we’re not done learning about callbacks yet, we’ll come back to uses of after_find
later on in the chapter, in the section “Modifying ActiveRecord
Classes at Runtime.”
It is common enough to want to reuse callback code for more than one object that Rails gives you a way to write callback classes. All you have to do is pass a given callback queue an object that responds to the name of the callback and takes the model object as a parameter.
Here’s our paranoid example from the previous section as a callback class:
class MarkDeleted def self.before_destroy(model) model.update_attribute(:deleted_at, Time.now) and return false end end
The behavior of MarkDeleted
is stateless, so I added the callback as a class method. Now you don’t have to instantiate MarkDeleted
objects for no good reason. All you do is pass the class to the callback queue for whichever models you want to have the mark-deleted behavior:
class Account < ActiveRecord::Base before_destroy MarkDeleted ... end class Invoice < ActiveRecord::Base before_destroy MarkDeleted ... end
There’s no rule that says you can’t have more than one callback method in a callback class. For example, you might have special audit log requirements to implement:
class Auditor def initialize(audit_log) @audit_log = audit_log end def after_create(model) @audit_log.created(model.inspect) end def after_update(model) @audit_log.updated(model.inspect) end def after_destroy(model) @audit_log.destroyed(model.inspect) end end
To add audit logging to an ActiveRecord
class, you would do the following:
class Account < ActiveRecord::Base after_create Auditor.new(DEFAULT_AUDIT_LOG) after_update Auditor.new(DEFAULT_AUDIT_LOG) after_destroy Auditor.new(DEFAULT_AUDIT_LOG) ... end
Wow, that’s kind of ugly, having to add three Auditor
s on three lines. We could extract a local variable called auditor
, but it would still be repetitive. This might be an opportunity to take advantage of Ruby’s open classes, the fact that you can modify classes that aren’t part of your application.
Wouldn’t it be better to simply say acts_as_audited
at the top of the model that needs auditing? We can quickly add it to the ActiveRecord::Base
class, so that it’s available for all our models.
On my projects, the file where “quick and dirty” code like the method in Listing 9.1 would reside is lib/core_ext/active_record_base.rb
, but you can put it anywhere you want. You could even make it a plugin (as detailed in Chapter 19, “Extending Rails with Plugins”). Just make sure to require it from config/environment.rb
or it’ll never get loaded.
Now, the top of Account
is a lot less cluttered:
class Account < ActiveRecord::Base acts_as_audited ... end
When you add callback methods to a model class, you pretty much have to test that they’re functioning correctly in conjunction with the model to which they are added. That may or may not be a problem. In contrast, callback classes are super-easy to test in isolation.
The following test method verifies correct operation of our Auditor
callback class (using the Mocha mocking library available at http://mocha.rubyforge.org/):
def test_auditor_logs_created (model = mock).expects(:inspect).returns('foo') (log = mock).expects(:created).with('foo') Auditor.new(log).after_create(model) end
Chapter 17, “Testing,” and Chapter 18, “RSpec on Rails,” cover testing with Test::Unit
and RSpec
, respectively.
The single responsibility principle is a very important tenet of object-oriented programming. It compels us to keep a class focused on a single concern. As you’ve learned in the previous section, callbacks are a useful feature of ActiveRecord
models that allow us to hook in behavior at various points of a model object’s life cycle. Even if we pull that extra behavior out into callback classes, the hook still requires code changes in the model class definition itself. On the other hand, Rails gives us a way to hook in that is completely transparent to the model class: Observers
.
Here is the functionality of our old Auditor
callback class as an observer of Account
objects:
class AccountObserver < ActiveRecord::Observer def after_create(model) DEFAULT_AUDIT_LOG.created(model.inspect) end def after_update(model) DEFAULT_AUDIT_LOG.updated(model.inspect) end def after_destroy(model) DEFAULT_AUDIT_LOG.destroyed(model.inspect) end end
When ActiveRecord::Observer
is subclassed, it breaks down the name of the subclass by stripping off the “Observer” part. In the case of our AccountObserver
in the preceding example, it would know that you want to observe the Account
class. However, that’s not always desirable behavior. In fact, with general-purpose code such as our Auditor
, it’s positively a step backward, so it’s possible to overrule the naming convention with the use of the observe
macro-style method. We still extend ActiveRecord::Observer
, but we can call the subclass whatever we want and tell it explicitly what to observe:
class Auditor < ActiveRecord::Observer observe Account, Invoice, Payment def after_create(model) DEFAULT_AUDIT_LOG.created(model.inspect) end def after_update(model) DEFAULT_AUDIT_LOG.updated(model.inspect) end def after_destroy(model) DEFAULT_AUDIT_LOG.destroyed(model.inspect) end end
If there weren’t a place for you to tell Rails which observers to load, they would never get loaded at all, since they’re not referenced from any other code in your application. As mentioned in Chapter 1, “Rails Environments and Configuration,” your application’s boilerplate config/environment.rb
has a commented-out line where you should define the observers to be loaded:
# Activate observers that should always be running config.active_record.observers = [:auditor]
A lot of applications start out with a User
model of some sort. Over time, as different kinds of users emerge, it might make sense to make a greater distinction between them. Admin
and Guest
classes are introduced, as subclasses of User
. Now, the shared behavior can reside in User
, and subtype behavior can be pushed down to subclasses. However, all user data can still reside in the users
table—all you need to do is introduce a type
column that will hold the name of the class to be instantiated for a given row.
To continue explaining single-table inheritance, let’s turn back to our example of a recurring Timesheet
class. We need to know how many billable_hours
are outstanding for a given user. The calculation can be implemented in various ways, but in this case we’ve chosen to write a pair of class and instance methods on the Timesheet
class:
class Timesheet < ActiveRecord::Base ... def billable_hours_outstanding if submitted? billable_weeks.map(&:total_hours).sum else 0 end end def self.billable_hours_outstanding_for(user) user.timesheets.map(&:billable_hours_outstanding).sum end end
I’m not suggesting that this is good code. It works, but it’s inefficient and that if/else
condition is a little fishy. Its shortcomings become apparent once requirements emerge about marking a Timesheet
as paid. It forces us to modify Timesheet
’s billable_hours_outstanding
method again:
def billable_hours_outstanding if submitted? and not paid? billable_weeks.map(&:total_hours).sum else 0 end end
That latest change is a clear violation of the open-closed principle,[5] which urges you to write code that is open for extension, but closed for modification. We know that we violated the principle, because we were forced to change the billable_hours_outstanding
method to accommodate the new Timesheet
status. Though it may not seem like a large problem in our simple example, consider the amount of conditional code that will end up in the Timesheet
class once we start having to implement functionality such as paid_hours
and unsubmitted_hours
.
So what’s the answer to this messy question of the constantly changing conditional? Given that you’re reading the section of the book about single-table inheritance, it’s probably no big surprise that we think one good answer is to use object-oriented inheritance. To do so, let’s break our original Timesheet
class into four classes.
class Timesheet < ActiveRecord::Base # non-relevant code ommitted def self.billable_hours_outstanding_for(user) user.timesheets.map(&:billable_hours_outstanding).sum end end class DraftTimesheet < Timesheet def billable_hours_outstanding 0 end end class SubmittedTimesheet < Timesheet def billable_hours_outstanding billable_weeks.map(&:total_hours).sum end end
Now when the requirements demand the ability to calculate partially paid timesheets, we need only add some behavior to a PaidTimesheet
class. No messy conditional statements in sight!
class PaidTimesheet < Timesheet def billable_hours_outstanding billable_weeks.map(&:total_hours).sum - paid_hours end end
Mapping object inheritance effectively to a relational database is not one of those problems with a definitive solution. We’re only going to talk about the one mapping strategy that Rails supports natively, which is single-table inheritance, called STI for short.
In STI, you establish one table in the database to holds all of the records for any object in a given inheritance hierarchy. In ActiveRecord
STI, that one table is named after the top parent class of the hierarchy. In the example we’ve been considering, that table would be named timesheets
.
Hey, that’s what it was called before, right? Yes, but to enable STI we have to add a type
column to contain a string representing the type of the stored object. The following migration would properly set up the database for our example:
class AddTypeToTimesheet < ActiveRecord::Migration def self.up add_column :timesheets, :type, :string end def self.down remove_column :timesheets, :type end end
No default value is needed. Once the type column is added to an ActiveRecord
model, Rails will automatically take care of keeping it populated with the right value. Using the console, we can see this behavior in action:
>> d = DraftTimesheet.create >> d.type => 'DraftTimesheet'
When you try to find an object using the find
methods of a base STI class, Rails will automatically instantiate objects using the appropriate subclass. This is especially useful in cases such as the timesheet example we’ve been describing, where we retrieve all the records for a particular user and then call methods that behave differently depending on the object’s class.
>> Timesheet.find(:first) => #<DraftTimesheet:0x2212354...>
Although Rails makes it extremely simple to use single-table inheritance, there are a few caveats that you should keep in mind.
To begin with, you cannot have an attribute on two different subclasses with the same name but a different type. Since Rails uses one table to store all subclasses, these attributes with the same name occupy the same column in the table. Frankly, there’s not much of a reason why that should be a problem unless you’ve made some pretty bad data-modeling decisions.
More importantly, you need to have one column per attribute on any subclass and any attribute that is not shared by all the subclasses must accept nil values. In the recurring example, PaidTimesheet
has a paid_hours
column that is not used by any of the other subclasses. DraftTimesheet
and SubmittedTimesheet
will not use the paid_hours
column and leave it as null in the database. In order to validate data for columns not shared by all subclasses, you must use ActiveRecord
validations and not the database.
Third, it is not a good idea to have subclasses with too many unique attributes. If you do, you will have one database table with many null values in it. Normally, a tree of subclasses with a large number of unique attributes suggests that something is wrong with your application design and that you should refactor. If you have an STI table that is getting out of hand, it is time to reconsider your decision to use inheritance to solve your particular problem. Perhaps your base class is too abstract?
Finally, legacy database constraints may require a different name in the database for the type
column. In this case, you can set the new column name using the class method set_inheritance_column
in the base class. For the Timesheet
example, we could do the following:
class Timesheet < ActiveRecord::Base set_inheritance_column 'object_type' end
Now Rails will automatically populate the object_type
column with the object’s type.
It seems pretty common for applications, particularly data-management ones, to have models that are very similar in terms of their data payload, mostly varying in their behavior and associations to each other. If you used object-oriented languages prior to Rails, you’re probably already accustomed to breaking down problem domains into hierarchical structures.
Take for instance, a Rails application that deals with the population of states, counties, cities, and neighborhoods. All of these are places, which might lead you to define an STI class named Place
as shown in Listing 9.2. I’ve also included the database schema for clarity:[6]
Example 9.2. The Places Database Schema and the Place
Class
# == Schema Information # # Table name: places # # id :integer(11) not null, primary key # region_id :integer(11) # type :string(255) # name :string(255) # description :string(255) # latitude :decimal(20, 1) # longitude :decimal(20, 1) # population :integer(11) # created_at :datetime # updated_at :datetime class Place < ActiveRecord::Base end
Place
is in essence an abstract class. It should not be instantiated, but there is no foolproof way to enforce that in Ruby. (No big deal, this isn’t Java!) Now let’s go ahead and define concrete subclasses of Place
:
class State < Place has_many :counties, :foreign_key => 'region_id' has_many :cities, :through => :counties end class County < Place belongs_to :state, :foreign_key => 'region _id' has_many :cities, :foreign_key => 'region _id' end class City < Place belongs_to :county, :foreign_key => 'region _id' end
You might be tempted to try adding a cities
association to State
, knowing that has_many :through
works with both belongs_to
and has_many
target associations. It would make the State
class look something like this:
class State < Place has_many :counties, :foreign_key => 'region_id' has_many :cities, :through => :counties end
That would certainly be cool, if it worked. Unfortunately, in this particular case, since there’s only one underlying table that we’re querying, there simply isn’t a way to distinguish among the different kinds of objects in the query:
Mysql::Error: Not unique table/alias: 'places': SELECT places.* FROM places INNER JOIN places ON places.region_id = places.id WHERE ((places.region_id = 187912) AND ((places.type = 'County'))) AND ((places.`type` = 'City' ))
What would we have to do to make it work? Well, the most realistic would be to use specific foreign keys, instead of trying to overload the meaning of region_id
for all the subclasses. For starters, the places
table would look like the example in Listing 9.3.
Example 9.3. The Places Database Schema, Revised
# == Schema Information # # Table name: places # # id :integer(11) not null, primary key # state_id :integer(11) # county_id :integer(11) # type :string(255) # name :string(255) # description :string(255) # latitude :decimal(20, 1) # longitude :decimal(20, 1) # population :integer(11) # created_at :datetime # updated_at :datetime
The subclasses would be simpler without the :foreign_key
options on the associations. Plus you could use a regular has_many
relationship from State
to City
, instead of the more complicated has_many :through
.
class State < Place has_many :counties has_many :cities end class County < Place belongs_to :state has_many :cities end class City < Place belongs_to :county end
Of course, all those null columns in the places table won’t win you any friends with relational database purists. That’s nothing, though. Just a little bit later in this chapter we’ll take a second, more in-depth look at polymorphic has_many
relationships, which will make the purists positively hate you.
In contrast to single-table inheritance, it is possible for ActiveRecord
models to share common code via inheritance and still be persisted to different database tables. The technique involves creating an abstract base model class that persistent subclasses will extend. It’s actually one of the simpler techniques that we broach in this chapter.
Let’s take the Place
class from the previous section (refer to Listing 9.3) and revise it to be an abstract base class in Listing 9.4. It’s simple really—we just have to add one line of code:
As I said, quite simple. Marking an ActiveRecord
model abstract is essentially the opposite of making it an STI class with a type column. You’re telling Rails: “Hey, I don’t want you to assume that there is a table named places
.”
In our running example, it means we would have to establish tables for states, counties, and cities, which might be exactly what we want. Remember though, that we would no longer be able to query across subtypes with code like Place.find(:all)
.
Abstract classes is an area of Rails where there aren’t too many hard-and-fast rules to guide you—experience and gut feeling will help you out.
In case you haven’t noticed yet, both class and instance methods are shared down the inheritance hierarchy of ActiveRecord
models. So are constants and other class members brought in through module inclusion. That means we can put all sorts of code inside Place
that will be useful to its subclasses.
Rails gives you the ability to make one class belong_to
more than one type of another class, as eloquently stated by blogger Mike Bayer:
The “polymorphic association,” on the other hand, while it bears some resemblance to the regular polymorphic union of a class hierarchy, is not really the same since you’re only dealing with a particular association to a single target class from any number of source classes, source classes which don’t have anything else to do with each other; i.e. they aren’t in any particular inheritance relationship and probably are all persisted in completely different tables. In this way, the polymorphic association has a lot less to do with object inheritance and a lot more to do with aspect-oriented programming (AOP); a particular concept needs to be applied to a divergent set of entities which otherwise are not directly related. Such a concept is referred to as a cross-cutting concern, such as, all the entities in your domain need to support a history log of all changes to a common logging table. In the AR example, an Order and a User object are illustrated to both require links to an Address object.[7]
In other words, this is not polymorphism in the typical object-oriented sense of the word; rather, it is something unique to Rails.
In our recurring Time and Expenses example, let’s assume that we want both BillableWeek
and Timesheet
to have many comments (a shared Comment
class). A naive way to solve this problem might be to have the Comment
class belong to both the BillableWeek
and Timesheet
classes and have billable_week_id
and timesheet_id
as columns in its database table.
class Comment < ActiveRecord::Base belongs_to :timesheet belongs_to :expense_report end
That approach is naive because it would be difficult to work with and hard to extend. Among other things, you would need to add code to the application to ensure that a Comment
never belonged to both a BillableWeek
and a Timesheet
at the same time. The code to figure out what a given comment is attached to would be cumbersome to write. Even worse, every time you want to be able to add comments to another type of class, you’d have to add another nullable foreign key column to the comments table.
Rails solves this problem in an elegant fashion, by allowing us to define what it terms polymorphic associations, which we covered when we described the :polymorphic => true
option of the belongs_to
association in Chapter 7, “ActiveRecord
Associations.”
Using a polymorphic association, we need define only a single belongs_to
and add a pair of related columns to the underlying database table. From that moment on, any class in our system can have comments attached to it (which would make it commentable), without needing to alter the database schema or the Comment
model itself.
class Comment < ActiveRecord::Base belongs_to :commentable, :polymorphic => true end
There isn’t a Commentable
class (or module) in our application. We named the association :commentable
because it accurately describes the interface of objects that will be associated in this way. The name :commentable
will turn up again on the other side of the association:
class Timesheet < ActiveRecord::Base has_many :comments, :as => :commentable end class BillableWeek < ActiveRecord::Base has_many :comments, :as => :commentable end
Here we have the friendly has_many
association using the :as
option. The :as
marks this association as polymorphic, and specifies which interface we are using on the other side of the association. While we’re on the subject, the other end of a polymorphic belongs_to
can be either a has_many
or a has_one
and work identically.
Here’s a migration that will create the comments
table:
class CreateComments < ActiveRecord::Migration def self.up create_table :comments do |t| t.column :text, :text t.column :commentable_id, :integer t.column :commentable_type, :string end end end
As you can see, there is a column called commentable_type
, which stores the class name of associated object. We can see how this works using the Rails console:
>> c = Comment.create(:text => "I could be commenting anything.") >> t = TimeSheet.create >> b = BillableWeek.create >> c.update_attribute(:commentable, t) => true >> "#{c.commentable_type}: #{c.commentable_id}" => "Timesheet: 1" >> c.update_attribute(:commentable, b) => true >> "#{c.commentable_type}: #{c.commentable_id}" => "BillableWeek: 1"
As you can tell, both the Timesheet
and the BillableWeek
that we played with in the console had the same id (1). Thanks to the commentable_type
attribute, stored as a string, Rails can figure out which is the related object.
There are some logical limitations that come into play with polymorphic associations. For instance, since it is impossible for Rails to know the tables necessary to join through a polymorphic association, the following hypothetical code will not work.
class Comment < ActiveRecord::Base belongs_to :user belongs_to :commentable, :polymorphic => true end class User < ActiveRecord::Base has_many :comments has_many :commentables, :through => :comments end >> User.find(:first).comments ActiveRecord::HasManyThroughAssociationPolymorphicError: Cannot have a has_many :through association 'User#commentables' on the polymorphic object 'Comment#commentable'.
If you really need it, has_many :through
is possible with polymorphic associations, but only by specifying exactly what type of polymorphic associations you want. To do so, you must use the :source_type
option. In most cases, you will also need to use the :source
option, since the association name will not match the interface name used for the polymorphic association:
class User < ActiveRecord::Base has_many :comments has_many :commented_timesheets, :through => :comments, :source => :commentable, :source_type => 'Timesheet' has_many :commented_billable_weeks, :through => :comments, :source => :commentable, :source_type => 'BillableWeek' end
It’s verbose, and the whole thing is arguably starting to lose its elegance if you go this route, but it works:
>> User.find(:first).commented_timesheets => [#<Timesheet:0x575b98 @attributes={}> ]
As we work toward the end of this book’s coverage of ActiveRecord
, you might have noticed that we haven’t really touched on a subject of particular importance to many programmers: foreign-key constraints in the database. That’s mainly because use of foreign-key constraints simply isn’t the Rails way to tackle the problem of relational integrity. To put it mildly, that opinion is controversial and some developers have written off Rails (and its authors) for expressing it.
There really isn’t anything stopping you from adding foreign-key constraints to your database tables, although you’d do well to wait until after the bulk of development is done. The exception, of course, is those polymorphic associations, which are probably the most extreme manifestation of the Rails opinion against foreign-key constraints. Unless you’re armed for battle, you might not want to broach that particular subject with your DBA.
In this section, we’ll talk about one strategy for breaking out functionality that is shared between disparate model classes. Instead of using inheritance, we’ll put the shared code into modules.
In the section “Polymorphic has_many
Relationships,” we described how to add a commenting feature to our recurring sample Time and Expenses application. We’ll continue fleshing out that example, since it lends itself to factoring out into modules.
The requirements we’ll implement are as follows: Both users and approvers should be able to add their comments to a Timesheet
or ExpenseReport
. Also, since comments are indicators that a timesheet or expense report requires extra scrutiny or processing time, administrators of the application should be able to easily view a list of recent comments. Human nature being what it is, administrators occasionally gloss over the comments without actually reading them, so the requirements specify that a mechanism should be provided for marking comments as “OK” first by the approver, then by the administrator.
Again, here is the polymorphic has_many :as
that we used as the foundation for this functionality:
class Timesheet < ActiveRecord::Base has_many :comments, :as => :commentable end class ExpenseReport < ActiveRecord::Base has_many :comments, :as => :commentable end class Comment < ActiveRecord::Base belongs_to :commentable, :polymorphic => true end
Next we create a controller and action for the administrator that list the 10 most recent comments with links to the item to which they are attached.
class RecentCommentsController < ApplicationController def show @recent_comments = Comment.find( :all, :limit => 10, :order => 'created_at DESC' ) end end
Here’s some of the simple view template used to display the recent comments.
<ul> <% @recent_comments.each do |comment| %> <li> <h4><%= comment.created_at -%></h4> <%= comment.text %> <div class="meta"> Comment on: <%= link_to comment.commentable.title, content_url( comment.commentable ) -%> </div> </li> <% end %> </ul>
So far, so good. The polymorphic association makes it easy to access all types of comments in one listing. But remember each comment needs to be marked “OK” by the approver and/or administrator. Comments should not appear once they’ve been marked as reviewed.
We won’t go into the comment approval interface here. Suffice it to say that a Comment
has a reviewed
attribute that returns true
after it has been marked “OK.”
In order to find all of the unreviewed comments for an item, we can use an association extension by modifying the model class definitions as follows:
class Timesheet < ActiveRecord::Base has_many :comments, :as => :commentable do def approved find(:all, :conditions => {:reviewed => false }) end end end class ExpenseReport < ActiveRecord::Base has_many :comments, :as => :commentable do def approved find(:all, :conditions => {:reviewed => false }) end end end
I’m not happy with this code and I hope by now you know why. It’s not DRY! Both Timesheet
and ExpenseReport
currently have their own identical methods for finding unreviewed comments. Essentially, they both share a common interface. They’re commentable!
The way that we define common interfaces that share code in Ruby is to include a module in each of those classes, where the module contains the code common to all implementations of the common interface.
So let’s go ahead and define a Commentable
module to do just that, and include it in our model classes:
module Commentable has_many :comments, :as => :commentable do def approved find( :all, :conditions => ['approved = ?', true ] ) end end end class Timesheet < ActiveRecord::Base include Commentable end class ExpenseReport < ActiveRecord::Base include Commentable end
Whoops, this code doesn’t work! To fix it, we need to understand an essential aspect of the way that Ruby interprets our code dealing with open classes.
In many other interpreted, OO programming languages, you have two phases of execution—one in which the interpreter loads the class definitions and says “this is the definition of what I have to work with,” followed by the phase in which it executes the code. This makes it difficult (though not necessarily impossible) to add new methods to a class dynamically during execution.
In contrast, Ruby lets you add methods to a class at any time. In Ruby, when you type class MyClass
, you’re doing more than simply telling the interpreter to define a class; you’re telling it to “execute the following code in the scope of this class.”
Let’s say you have the following Ruby script:
1 class Foo < ActiveRecord::Base 2 has_many :bars 3 end 4 class Foo 5 belongs_to :spam 6 end
When the interpreter gets to line 1, you are telling it to execute the following code (up to the matching end) in the context of the Foo
class object. Because the Foo
class object doesn’t exist yet, it goes ahead and creates the class. At line 2, we execute the statement has_many :bars
in the context of the Foo
class object. Whatever the has_many
message does, it does right now.
When we again say class Foo
at line 4, we are once again telling the interpreter to execute the following code in the context of the Foo
class object, but this time, the interpreter already knows about class Foo
; it doesn’t actually create another class. Therefore, on line 5, we are simply telling the interpreter to execute the belongs_to :spam
statement in the context of that same Foo
class object.
In order to execute the has_many
and belongs_to
statements, those methods need to exist in the context in which they are executed. Because these are defined as class methods in ActiveRecord::Base
, and we have previously defined class Foo
as extending ActiveRecord::Base
, the code will execute without a problem.
However, when we defined our Commentable
module like this:
module Commentable has_many :comments, :as => :commentable do def approved find( :all, :conditions => ['approved = ?', true ] ) end end end
...we get an error when it tries to execute the has_many
statement. That’s because the has_many
method is not defined in the context of the Commentable
module object.
Given what we now know about how Ruby is interpreting the code, we now realize that what we really want is for that has_many
statement to be executed in the context of the including class.
Luckily, Ruby’s Module
class defines a handy callback that we can use to do just that. If a Module
object defines the method included
, it gets run whenever that module is included in another module or class. The argument passed to this method is the module/class object into which this module is being included.
We can define an included
method on our Commentable
module object so that it executes the has_many
statement in the context of the including class (Timesheet
, ExpenseReport
, and so on):
module Commentable def self.included(base) base.class_eval do has_many :comments, :as => :commentable do def approved find(:all, :conditions => ['approved = ?', true ]) end end end end end
Now, when we include the Commentable module in our model classes, it will execute the has_many
statement just as if we had typed it into each of those classes’ bodies.
The metaprogramming capabilities of Ruby, combined with the after_find
callback, open the door to some interesting possibilities, especially if you’re willing to blur your perception of the difference between code and data. I’m talking about modifying the behavior of model classes on the fly, as they’re loaded into your application.
Listing 9.5 is a drastically simplified example of the technique, which assumes the presence of a config
column on your model. During the after_find
callback, we get a handle to the unique singleton class[8] of the model instance being loaded. Then we execute the contents of the config
attribute belonging to this particular Account
instance, using Ruby’s class_eval
method. Since we’re doing this using the singleton class for this instance, rather than the global Account
class, other account instances in the system are completely unaffected.
I used powerful techniques like this one in a supply-chain application that I wrote for a large industrial client. A lot is a generic term in the industry used to describe a shipment of product. Depending on the vendor and product involved, the attributes and business logic for a given lot vary quite a bit. Since the set of vendors and products being handled changed on a weekly (sometimes daily) basis, the system needed to be reconfigurable without requiring a production deployment.
Without getting into too much detail, the application allowed the maintenance programmers to easily customize the behavior of the system by manipulating Ruby code stored in the database, associated with whatever product the lot contained.
For example, one of the business rules associated with lots of butter being shipped for Acme Dairy Co. might dictate a strictly integral product code, exactly 10 digits in length. The code, stored in the database, associated with the product entry for Acme Dairy’s butter product would therefore contain the following two lines:
validates_numericality_of :product_code, :only_integer => true validates_length_of :product_code, :is => 10
A relatively complete description of everything you can do with Ruby metaprogramming, and how to do it correctly, would fill its own book. For instance, you might realize that doing things like executing arbitrary Ruby code straight out of the database is inherently dangerous. That’s why I emphasize again that the examples shown here are very simplified. All I want to do is give you a taste of the possibilities.
If you do decide to begin leveraging these kinds of techniques in real-world applications, you’ll have to consider security and approval workflow and a host of other important concerns. Instead of allowing arbitrary Ruby code to be executed, you might feel compelled to limit it to a small subset related to the problem at hand. You might design a compact API, or even delve into authoring a domain-specific language (DSL), crafted specifically for expressing the business rules and behaviors that should be loaded dynamically. Proceeding down the rabbit hole, you might write custom parsers for your DSL that could execute it in different contexts —some for error detection and others for reporting. It’s one of those areas where the possibilities are quite limitless.
My former colleague Jay Fields and I pioneered the mix of Ruby metaprogramming, Rails, and internal[9] domain-specific languages while doing Rails application development for ThoughtWorks clients. I still occasionally speak at conferences and blog about writing DSLs in Ruby.
Jay has also continued writing and speaking about his evolution of Ruby DSL techniques, which he calls Business Natural Languages (or BNL for short[10]). When developing BNLs, you craft a domain-specific language that is not necessarily valid Ruby syntax, but is close enough to be transformed easily into Ruby and executed at runtime, as shown in Listing 9.6.
Example 9.6. Example of Business Natural Language
employee John Doe compensate 500 dollars for each deal closed in the past 30 days compensate 100 dollars for each active deal that closed more than 365 days ago compensate 5 percent of gross profits if gross profits are greater than 1,000,000 dollars compensate 3 percent of gross profits if gross profits are greater than 2,000,000 dollars compensate 1 percent of gross profits if gross profits are greater than 3,000,000 dollars
The ability to leverage advanced techniques such as DSLs is yet another powerful tool in the hands of experienced Rails developers.
With this chapter we conclude our coverage of ActiveRecord
, one of the most significant and powerful Rails frameworks. We examined how callbacks and observers let us factor our code in a clean and object-oriented fashion. We also expanded our modeling options by considering single-table inheritance and ActiveRecord
’s distinctive polymorphic relationships.
At this point in the book, we’ve covered two parts of the MVC pattern: the model and the controller. It’s now time to delve into the third and final part: the view.
1. | If you are browsing old Rails source code, you might come across callback macros receiving a short string of Ruby code to be eval’d in the binding of the model object. That way of adding callbacks was deprecated in Rails 1.2, because you’re always better off using a block in those situations. |
2. | I recommend the excellent GeoKit for Rails plugin available at http://geokit.rubyforge.org/. |
3. | Real-life implementation of the example would also need to modify all finders to include |
4. | Get AttachmentFu at http://svn.techno-weenie.net/projects/plugins/attachment_fu. |
5. | http://en.wikipedia.org/wiki/Open/closed_principle has a good summary. |
6. | For autogenerated schema information added to the top of your model classes, try Dave Thomas’s annotate_models plugin at http://svn.pragprog.com/Public/plugins/annotate_models. |
7. | |
8. | I don’t expect this to make sense to you, unless you are familiar with Ruby’s singleton classes, and the ability to evaluate arbitrary strings of Ruby code at runtime. A good place to start is http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html. |
9. | The qualifier internal is used to differentiate a domain-specific language hosted entirely inside of a general-purpose language, such as Ruby, from one that is completely custom and requires its own parser implementation. |
10. | Googling BNL will give you tons of links to the Toronto-based band Barenaked Ladies, so you’re better off going directly to the source at http://bnl.jayfields.com. |
18.189.186.167