The Rails 4 Way (2014)
Chapter 9. Advanced Active Record
Respectful debate, honesty, passion, and working systems created an environment that not even the most die-hard enterprise architect could ignore, no matter how buried in Java design patterns. Those who placed technical excellence and pragmaticism above religious attachment and vendor cronyism were easily convinced of the benefits that broadening their definition of acceptable technologies could bring.21
—Ryan Tomayko (March 2006)
Active Record 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, Active Record 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 Active Record—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 Active Record by reviewing callbacks, 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 Active Record.
9.1 Scopes
Scopes (or “named scopes” if you’re old school) allow you define and chain query criteria in a declarative and reusable manner.
1 classTimesheet < ActiveRecord::Base
2 scope :submitted, -> { where(submitted: true) }
3 scope :underutilized, -> { where('total_hours < 40') }
To declare a scope, use the scope class method, passing it a name as a symbol and a callable object that includes a query criteria within. You can simply use Arel criteria methods such as where, order, and limit to construct the definition as shown in the example. The queries defined in a scope are only evaluated whenever the scope is invoked.
1 classUser < ActiveRecord::Base
2 scope :delinquent, -> { where('timesheets_updated_at < ?', 1.week.ago) }
Invoke scopes as you would class methods.
>> User.delinquent
=> [#<User id: 2, timesheets_updated_at: "2013-04-20 20:02:13"...>]
Note that instead of using the scope macro style method, you can simply define a class method on an Active Record model which returns a scoped method, such as where. To illustrate, the following class method is equivalent to the delinquent scope defined in the above example.
1 defself.delinquent
2 where('timesheets_updated_at < ?', 1.week.ago)
3 end
9.1.1 Scope Parameters
You can pass arguments to scope invocations by adding parameters to the proc you use to define the scope query.
1 classBillableWeek < ActiveRecord::Base
2 scope :newer_than, ->(date) { where('start_date > ?', date) }
Then pass the argument to the scope as you would normally.
BillableWeek.newer_than(Date.today)
9.1.2 Chaining Scopes
One of the benefits of scopes is that you can chain them together to create complex queries from simple ones:
>> Timesheet.underutilized.submitted.to_a
=> [#<Timesheet id: 3, submitted: true, total_hours: 37 ...
Scopes can be chained together for reuse within scope definitions themselves. For instance, let’s say that we always want to constrain the result set of underutilized to submitted timesheets:
1 classTimesheet < ActiveRecord::Base
2 scope :submitted, -> { where(submitted: true) }
3 scope :underutilized, -> { submitted.where('total_hours < 40') }
9.1.3 Scopes and has_many
In addition to being available at the class context, scopes are available automatically on has_many association attributes.
>> u = User.find(2)
=> #<User id: 2, username: "obie"...>
>> u.timesheets.size
=> 3
>> u.timesheets.underutilized.size
=> 1
9.1.4 Scopes and Joins
You can use Arel’s join method to create cross-model scopes. For instance, if we gave our recurring example Timesheet a submitted_at date attribute instead of just a boolean, we could add a scope to User allowing us to see who is late on their timesheet submission.
1 scope :tardy, -> {
2 joins(:timesheets).
3 where("timesheets.submitted_at <= ?", 7.days.ago).
4 group("users.id")
5 }
Arel’s to_sql method is useful for debugging scope definitions and usage.
>> User.tardy.to_sql
=> "SELECT "users".* FROM "users"
INNER JOIN "timesheets" ON "timesheets"."user_id" = "users"."id"
WHERE (timesheets.submitted_at <= '2013-04-13 18:16:15.203293')
GROUP BY users.id" # query formatted nicely for the book
Note that as demonstrated in the example, it’s a good idea to use unambiguous column references (including table name) in cross-model scope definitions so that Arel doesn’t get confused.
9.1.5 Scope Combinations
Our example of a cross-model scope violates good object-oriented design principles: it contains the logic for determining whether or not a Timesheet is submitted, which is code that properly belongs in the Timesheet class. Luckily we can use Arel’s merge method to fix it. First we put the late logic where it belongs, in Timesheet:
scope :late, -> { where("timesheet.submitted_at <= ?", 7.days.ago) }
Then we use our new late scope in tardy:
scope :tardy, -> {
joins(:timesheets).group("users.id").merge(Timesheet.late)
}
If you have trouble with this technique, make absolutely sure that your scopes’ clauses refer to fully qualified column names. (In other words, don’t forget to prefix column names with tables.) The console and to_sql method is your friend for debugging.
9.1.6 Default Scopes
There may arise use cases where you want certain conditions applied to the finders for your model. Consider our timesheet application has a default view of open timesheets - we can use a default scope to simplify our general queries.
classTimesheet < ActiveRecord::Base
default_scope { where(status: "open") }
end
Now when we query for our Timesheets, by default the open condition will be applied:
>> Timesheet.pluck(:status)
=> ["open", "open", "open"]
Default scopes also get applied to your models when building or creating them which can be a great convenience or a nuisance if you are not careful. In our previous example all new Timesheets will be created with a status of “open.”
>> Timesheet.new
=> #<Timesheet id: nil, status: "open">
>> Timesheet.create
=> #<Timesheet id: 1, status: "open">
You can override this behavior by providing your own conditions or scope to override the default setting of the attributes.
>> Timesheet.where(status: "new").new
=> #<Timesheet id: nil, status: "new">
>> Timesheet.where(status: "new").create
=> #<Timesheet id: 1, status: "new">
There may be cases where at runtime you want to create a scope and pass it around as a first class object leveraging your default scope. In this case Active Record provides the all method.
>> timesheets = Timesheet.all.order("submitted_at DESC")
=> #<ActiveRecord::Relation [#<Timesheet id: 1, status: "open"]>
>> timesheets.where(name: "Durran Jordan").to_a
=> []
There’s another approach to scopes that provides a sleeker syntax, scoping, which allows the chaining of scopes via nesting within a block.
>> Timesheet.order("submitted_at DESC").scoping do
>> Timesheet.first
>> end
=> #<Timesheet id: 1, status: "open">
That’s pretty nice, but what if we don’t want our default scope to be included in our queries? In this case Active Record takes care of us through the unscoped method.
>> Timesheet.unscoped.order("submitted_at DESC").to_a
=> [#<Timesheet id: 2, status: "submitted">]
Similarly to overriding our default scope with a relation when creating new objects, we can supply unscoped as well to remove the default attributes.
>> Timesheet.unscoped.new
=> #<Timesheet id: nil, status: nil>
9.1.7 Using Scopes for CRUD
You have a wide range of Active Record’s CRUD methods available on scopes, which gives you some powerful abilities. For instance, let’s give all our underutilized timesheets some extra hours.
>> u.timesheets.underutilized.pluck(:total_hours)
=> [37, 38]
>> u.timesheets.underutilized.update_all("total_hours = total_hours + 2")
=> 2
>> u.timesheets.underutilized.pluck(:total_hours)
=> [39]
Scopes including a where clause using hashed conditions will populate attributes of objects built off of them with those attributes as default values. Admittedly it’s a bit difficult to think of a plausible use case for this feature, but we’ll show it in an example. First, we add the following scope to Timesheet:
scope :perfect, -> { submitted.where(total_hours: 40) }
Now, building an object on the perfect scope should give us a submitted timesheet with 40 hours.
> Timesheet.perfect.build
=> #<Timesheet id: nil, submitted: true, user_id: nil, total_hours: 40 ...>
As you’ve probably realized by now, the Arel underpinnings of Active Record are tremendously powerful and truly elevate the Rails platform.
9.2 Callbacks
This advanced feature of Active Record allows the savvy developer to attach behavior at a variety of different points along a 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:
1 classBeethoven < ActiveRecord::Base
2 before_destroy :last_words
3
4 protected
5
6 def last_words
7 logger.info "Friends applaud, the comedy is over"
8 end
9 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.
9.2.1 One-Liners
Now, if (and only if) your callback routine is really short,22 you can add it by passing a block to the callback macro. We’re talking one-liners!
classNapoleon < ActiveRecord::Base
before_destroy { logger.info "Josephine..." }
...
end
Since Rails 3, the block passed to a callback is executed via instance_eval so that its scope is the record itself (versus needing to act on a passed in record variable). The following example implements “paranoid” model behavior, covered later in the chapter.
1 classAccount < ActiveRecord::Base
2 before_destroy { self.update_attribute(:deleted_at, Time.now); false }
3 ...
9.2.2 Protected or Private
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.
9.2.3 Matched before/after Callbacks
In total, there are 19 types of callbacks you can register on your models! Thirteen of them are matching before/after callback pairs, such as before_validation and after_validation. Four of them are around callbacks, such as around_save. (The other two, after_initialize and after_find, are special, and we’ll discuss them later in this section.)
9.2.3.1 List of Callbacks
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
· after_validation
· before_save
· around_save
· before_create (for new records) and before_update (for existing records)
· around_create (for new records) and around_update (for existing records)
· after_create (for new records) and after_update (for existing records)
· after_save
Delete operations have their own callbacks:
· before_destroy
· around_destroy executes a DELETE database statement on yield
· after_destroy is called after record has been removed from the database and all attributes have been frozen (read-only)
Callbacks may be limited to specific Active Record life cycles (:create, :update, :destroy), by explicitly defining which ones can trigger the it, using the :on option. The :on option may accept a single lifecycle (like on: :create) or an array of life cycles on: [:create, :update].
# Run only on create
before_validation :some_callback, on: :create
Additionally transactions have callbacks as well, for when you want actions to occur after the database is guaranteed to be in a permanent state. Note that only “after” callbacks exist here do to the nature of transactions - it’s a bad idea to be able to interfere with the actual operation itself.
· after_commit
· after_rollback
· after_touch
Skipping Callback Execution The following Active Record methods, when executed, do not run any callbacks: · decrement · decrement_counter · delete · delete_all · increment · increment_counter · toggle · touch · update_column · update_columns · update_all · update_counters |
9.2.4 Halting Execution
If you return a boolean false (not nil) from a callback method, Active Record 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.
9.2.5 Callback Usages
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.
9.2.5.1 Cleaning Up Attribute Formatting with before_validation on create
The most common examples of using before_validation callbacks have to do with cleaning up user-entered attributes. For example, the following CreditCard class cleans up its number attribute so that false negatives don’t occur on validation:
1 classCreditCard < ActiveRecord::Base
2 before_validation on: :create do
3 # Strip everything in the number except digits
4 self.number = number.gsub(/[^0-9]/, "")
5 end
6 end
9.2.5.2 Geocoding with before_save
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.23
As is often the case, the wording of the requirement itself points you in the direction of the before_save callback:
1 classAddress < ActiveRecord::Base
2
3 before_save :geocode
4 validates_presence_of :street, :city, :state, :country
5 ...
6
7 def to_s
8 [street, city, state, country].compact.join(', ')
9 end
10
11 protected
12
13 def geocode
14 result = Geocoder.coordinates(to_s)
15 self.latitude = result.first
16 self.longitude = result.last
17 end
18 end
Note For the sake of this example, we will not be using Geocoder’s Active Record extensions. |
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:
1 def geolocate
2 result = Geocoder.coordinates(to_s)
3 return false if result.empty? # halt execution
4
5 self.latitude = result.first
6 self.longitude = result.last
7 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:
1 def geolocate
2 result = Geocoder.coordinates(to_s)
3 if result.present?
4 self.latitude = result.first
5 self.longitude = result.last
6 else
7 errors[:base] << "Geocoding failed. Please check address."
8 false
9 end
10 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.
9.2.5.3 Exercise Your Paranoia with before_destroy
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 Active Record’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.
1 classAccount < ActiveRecord::Base
2 before_destroy do
3 self.update_attribute(:deleted_at, Time.current)
4 false
5 end
6
7 ...
8 end
After the deleted_at column is populated with the current time, we return false in the callback to halt execution. This ensures that the underlying record is not actually deleted from the database.24
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.
9.2.5.4 Cleaning Up Associated Files with after_destroy
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 thoughtbot’s Paperclip25 gem is a good example:
1 # Destroys the file. Called in an after_destroy callback
2 def destroy_attached_files
3 Paperclip.log("Deleting attachments.")
4 each_attachment do |name, attachment|
5 attachment.send(:flush_deletes)
6 end
7 end
9.2.6 Special Callbacks: after_initialize and after_find
The after_initialize callback is invoked whenever a new Active Record 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 Active Record 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:
1 after_initialize do
2 if new_record?
3 ...
4 end
5 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 Active Record 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:
1 classUser < ActiveRecord::Base
2 serialize :preferences # defaults to nil
3 ...
4
5 protected
6
7 def after_initialize
8 self.preferences ||= Hash.new
9 end
10 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.
Kevin says… You could change the above example to not use callbacks by using the Active Record store, a wrapper around serialize that is used exclusively for storing hashes in a database column. 1 classUser < ActiveRecord::Base 2 serialize :preferences # defaults to nil 3 store :preferences, accessors: [:show_help_text] 4 ... 5 end By default, the preferences attribute would be populated with an empty hash. Another added benefit is the ability to explicitly define accessors, removing the need to interact with the underlying hash directly. To illustrate, let’s set the show_help_text preference to true: >> user = User.new => #<User id: nil, properties: {}, ...> >> user.show_help_text = true => true >> user.properties => {"show_help_text" => true} |
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 Active Record Classes at Runtime.”
9.2.7 Callback Classes
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:
1 classMarkDeleted
2 defself.before_destroy(model)
3 model.update_attribute(:deleted_at, Time.current)
4 false
5 end
6 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 'line-height:normal'>1 classAccount < ActiveRecord::Base
2 before_destroyMarkDeleted
3 ...
4end
5
6 classInvoice < ActiveRecord::Base
7 before_destroyMarkDeleted
8 ...
9end
9.2.7.1 Multiple Callback Methods in One Class
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:
1 classAuditor
2 def initialize(audit_log)
3 @audit_log = audit_log
4 end
5
6 def after_create(model)
7 @audit_log.created(model.inspect)
8 end
9
10 def after_update(model)
11 @audit_log.updated(model.inspect)
12 end
13
14 def after_destroy(model)
15 @audit_log.destroyed(model.inspect)
16 end
17end
To add audit logging to an Active Record class, you would do the following:
1 classAccount < ActiveRecord::Base
2 after_create Auditor.new(DEFAULT_AUDIT_LOG)
3 after_update Auditor.new(DEFAULT_AUDIT_LOG)
4 after_destroy Auditor.new(DEFAULT_AUDIT_LOG)
5 ...
6end
Wow, that’s ugly, having to add three Auditors 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.
Listing 9.1: A quick-and-dirty ‘acts as audited’ method
1 classActiveRecord::Base
2 defself.acts_as_audited(audit_log=DEFAULT_AUDIT_LOG)
3 auditor = Auditor.new(audit_log)
4 after_create auditor
5 after_update auditor
6 after_destroy auditor
7 end
8 end
Now, the top of Account is a lot less cluttered:
1 classAccount < ActiveRecord::Base
2 acts_as_audited
9.2.7.2 Testability
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.
1 describe '#after_create' do
2 let(:auditable) { double() }
3 let(:log) { double() }
4 let(:content) { 'foo' }
5
6 it 'audits a model was created' do
7 expect(auditable).to receive(:inspect).and_return(content)
8 expect(log).to receive(:created).and_return(content)
9 Auditor.new(log).after_create(auditable)
10 end
11 end
9.3 Calculation Methods
All Active Record classes have a calculate method that provides easy access to aggregate function queries in the database. Methods for count, sum, average, minimum, and maximum have been added as convenient shortcuts.
Calculation methods can be used in combination with Active Record relation methods to customize the query. Since calculation methods do not return an ActiveRecord::Relation, they must be the last method in a scope chain.
There are two basic forms of output:
Single aggregate value
The single value is type cast to Fixnum for COUNT, Float for AVG, and the given column’s type for everything else.
Grouped values
This returns an ordered hash of the values and groups them by the :group option. It takes either a column name, or the name of a belongs_to association.
The following examples illustrate the usage of various calculation methods.
1 Person.calculate(:count, :all) # The same as Person.count
2
3 # SELECT AVG(age) FROM people
4 Person.average(:age)
5
6 # Selects the minimum age for everyone with a last name other than 'Drake'
7 Person.where.not(last_name: 'Drake').minimum(:age)
8
9 # Selects the minimum age for any family without any minors
10 Person.having('min(age) > 17').group(:last_name).minimum(:age)
9.3.1 average(column_name, *options)
Calculates the average value on a given column. The first parameter should be a symbol identifying the column to be averaged.
9.3.2 count(column_name, *options)
Count operates using three different approaches. Count without parameters will return a count of all the rows for the model. Count with a column_name will return a count of all the rows for the model with the supplied column present.
9.3.3 ids
Return all the ID’s for a relation based on its table’s primary key.
User.ids # SELECT id FROM "users"
9.3.4 maximum(column_name, *options)
Calculates the maximum value on a given column. The first parameter should be a symbol identifying the column to be calculated.
9.3.5 minimum(column_name, *options)
Calculates the minimum value on a given column. The first parameter should be a symbol identifying the column to be calculated.
9.3.6 pluck(*column_names)
The pluck method queries the database for one or more columns of the underlying table of a model.
>> User.pluck(:id, :name)
=> [[1, 'Obie']]
>> User.pluck(:name)
=> ['Obie']
It returns an array of values of the specified columns with the corresponding data type.
9.3.7 sum(column_name, *options)
Calculates a summed value in the database using SQL. The first parameter should be a symbol identifying the column to be summed.
9.4 Single-Table Inheritance (STI)
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:
1 classTimesheet < ActiveRecord::Base
2 ...
3
4 def billable_hours_outstanding
5 if submitted?
6 billable_weeks.map(&:total_hours).sum
7 else
8 0
9 end
10 end
11
12 defself.billable_hours_outstanding_for(user)
13 user.timesheets.map(&:billable_hours_outstanding).sum
14 end
15
16 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:
1 def billable_hours_outstanding
2 if submitted? && not paid?
3 billable_weeks.map(&:total_hours).sum
4 else
5 0
6 end
7 end
That latest change is a clear violation of the open-closed principle,26 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.
1 classTimesheet < ActiveRecord::Base
2 # non-relevant code ommitted
3
4 defself.billable_hours_outstanding_for(user)
5 user.timesheets.map(&:billable_hours_outstanding).sum
6 end
7 end
8
9 classDraftTimesheet < Timesheet
10 def billable_hours_outstanding
11 0
12 end
13 end
14
15 classSubmittedTimesheet < Timesheet
16 def billable_hours_outstanding
17 billable_weeks.map(&:total_hours).sum
18 end
19 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!
1 classPaidTimesheet < Timesheet
2 def billable_hours_outstanding
3 billable_weeks.map(&:total_hours).sum - paid_hours
4 end
5 end
9.4.1 Mapping Inheritance to the Database
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 Active Record 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:
1 classAddTypeToTimesheet < ActiveRecord::Migration
2 def change
3 add_column :timesheets, :type, :string
4 end
5 end
No default value is needed. Once the type column is added to an Active Record 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 query methods of a base STI class, Rails will automatically instantiate objects using the appropriate subclass. This is especially useful in polymorphic situations, 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.first
=> #<DraftTimesheet:0x2212354...>
Note
Rails won’t complain about the missing column; it will simply ignore it. Recently, the error message was reworded with a better explanation, but too many developers skim error messages and then spend an hour trying to figure out what’s wrong with their models. (A lot of people skim sidebar columns too when reading books, but hey, at least I am doubling their chances of learning about this problem.)
9.4.2 STI Considerations
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 andSubmittedTimesheet 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 Active Record 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 setter method inheritance_column in the base class. For the Timesheet example, we could do the following:
1 classTimesheet < ActiveRecord::Base
2 self.inheritance_column = 'object_type'
3 end
Now Rails will automatically populate the object_type column with the object’s type.
9.4.3 STI and Associations
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:27
Listing 9.2: The places database schema and the place class
1 # == Schema Information
2 #
3 # Table name: places
4 #
5 # id :integer(11) not null, primary key
6 # region_id :integer(11)
7 # type :string(255)
8 # name :string(255)
9 # description :string(255)
10 # latitude :decimal(20, 1)
11 # longitude :decimal(20, 1)
12 # population :integer(11)
13 # created_at :datetime
14 # updated_at :datetime
15
16 classPlace < ActiveRecord::Base
17 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:
1 classState < Place
2 has_many :counties, foreign_key: 'region_id'
3 end
4
5 classCounty < Place
6 belongs_to :state, foreign_key: 'region_id'
7 has_many :cities, foreign_key: 'region_id'
8 end
9
10 classCity < Place
11 belongs_to :county, foreign_key: 'region_id'
12 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:
1 classState < Place
2 has_many :counties, foreign_key: 'region_id'
3 has_many :cities, through: :counties
4 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.
Listing 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.
1 classState < Place
2 has_many :counties
3 has_many :cities
4 end
5
6 classCounty < Place
7 belongs_to :state
8 has_many :cities
9 end
10
11 classCity < Place
12 belongs_to :county
13 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.
9.5 Abstract Base Model Classes
In contrast to single-table inheritance, it is possible for Active Record models to share common code via inheritance and still be persisted to different database tables. In fact, every Rails developer uses an abstract model in their code whether they realize it or not: ActiveRecord::Base28.
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 inListing 9.4. It’s simple really—we just have to add one line of code:
Listing 9.4: The abstract place class
1 classPlace < ActiveRecord::Base
2 self.abstract_class = true
3 end
Marking an Active Record 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.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 Active Record 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.
9.6 Polymorphic has_many Relationships
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.29
In other words, this is not polymorphism in the typical object-oriented sense of the word; rather, it is something unique to Rails.
9.6.1 In the Case of Models with Comments
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 havebillable_week_id and timesheet_id as columns in its database table.
1 classComment < ActiveRecord::Base
2 belongs_to :timesheet
3 belongs_to :expense_report
4 end
I call 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, “Active Record Associations”.
9.6.1.1 The Interface
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.
1 classComment < ActiveRecord::Base
2 belongs_to :commentable, polymorphic: true
3 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:
1 classTimesheet < ActiveRecord::Base
2 has_many :comments, as: :commentable
3 end
4
5 classBillableWeek < ActiveRecord::Base
6 has_many :comments, as: :commentable
7 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_manyor a has_one and work identically.
9.6.1.2 The Database Columns
Here’s a migration that will create the comments table:
1 classCreateComments < ActiveRecord::Migration
2 def change
3 create_table :comments do |t|
4 t.text :body
5 t.integer :commentable
6 t.string :commentable_type
7 end
8 end
9 end
As you can see, there is a column called commentable_type, which stores the class name of associated object. The Migrations API actually gives you a one-line shortcut with the references method, which takes a polymorphic option:
1 create_table :comments do |t|
2 t.text :body
3 t.references :commentable, polymorphic: true
4 end
We can see how it comes together using the Rails console (some lines ommitted for brevity):
>> c = Comment.create(body: '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 correct related object.
9.6.1.3 Has_many :through and Polymorphics
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, which tries to find everything that the user has commented on, will not work.
1 classComment < ActiveRecord::Base
2 belongs_to :user # author of the comment
3 belongs_to :commentable, polymorphic: true
4 end
5
6 classUser < ActiveRecord::Base
7 has_many :comments
8 has_many :commentables, through: :comments
9 end
10
11 >> User.first.commentables
12 ActiveRecord::HasManyThroughAssociationPolymorphicSourceError: Cannot have a
13 has_many :through association 'User#commentables' on the polymorphic object
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:
1 classUser < ActiveRecord::Base
2 has_many :comments
3 has_many :commented_timesheets, through: :comments,
4 source: :commentable, source_type: 'Timesheet'
5 has_many :commented_billable_weeks, through: :comments,
6 source: :commentable, source_type: 'BillableWeek'
7 end
It’s verbose, and the whole scheme loses its elegance if you go this route, but it works:
>> User.first.commented_timesheets.to_a
=> [#<Timesheet ...>]
9.7 Enums
One of the newest additions to Active Record introduced in Rails 4.1 is the ability to set an attribute as an enumerable. Once an attribute has been set as an enumerable, Active Record will restrict the assignment of the attribute to a collection of predefined values.
To declare an enumerable attribute, use the enum macro style class method, passing it an attribute name and an array of status values that the attribute can be set to.
1 classPost < ActiveRecord::Base
2 enum status: %i(draft published archived)
3 ...
4 end
Active Record implicitly maps each predefined value of an enum attribute to an integer, therefore the column type of the enum attribute must be an integer as well. By default, an enum attribute will be set to nil. To set an initial state, one can set a default value in a migration. It’s recommended to set this value to the first declared status, which would map to 0.
1 classCreatePosts < ActiveRecord::Migration
2 def change
3 create_table :posts do |t|
4 t.integer :status, default: 0
5 end
6 end
7 end
For instance, given our example, the default status of a Post model would be “draft”:
>> Post.new.status
=> "draft"
You should never have to work with the underlying integer data type of an enum attribute, as Active Record creates both predicate and bang methods for each status value.
1 post.draft!
2 post.draft? # => true
3 post.published? # => false
4 post.status # => "draft"
5
6 post.published!
7 post.published? # => true
8 post.draft? # => false
9 post.status # => "published"
10
11 post.status = nil
12 post.status.nil? # => true
13 post.status # => nil
Active Record also provides scope methods for each status value. Invoking one of these scopes will return all records with that given status.
Post.draft
# Post Load (0.1ms) SELECT "posts".* FROM "posts"
WHERE "posts"."status" = 0
Note
Active Record creates a class method with a pluralized name of the defined enum on the model, that returns a hash with the key and value of each status. In our preceding example, the Post model would have a class method named statuses.
>> Post.statuses
=> {"draft"=>0, "published"=>1, "archived"=>2}
You should only need to access this class method when you need to know the underlying ordinal value of an enum.
With the addition of the enum attribute, Active Record finally has a simple state machine out of the box. This feature alone should simplify models that had previously depended on multiple boolean fields to manage state. If you require more advanced functionality, such as status transition callbacks and conditional transitions, it’s still recommended to use a full-blown state machine like s30.
9.8 Foreign-key Constraints
As we work toward the end of this book’s coverage of Active Record, 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.
9.9 Modules for Reusing Common Behavior
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 :comments, as: :commentable that we used as the foundation for this functionality:
1 classTimesheet < ActiveRecord::Base
2 has_many :comments, as: :commentable
3 end
4
5 classExpenseReport < ActiveRecord::Base
6 has_many :comments, as: :commentable
7 end
8
9 classComment < ActiveRecord::Base
10 belongs_to :commentable, polymorphic: true
11 end
Next we enable the controller and action for the administrator that list the 10 most recent comments with links to the item to which they are attached.
1 classComment < ActiveRecord::Base
2 scope :recent, -> { order('created_at desc').limit(10) }
3 end
4
5 classCommentsController < ApplicationController
6 before_action :require_admin, only: :recent
7 expose(:recent_comments) { Comment.recent }
8 end
Here’s some of the simple view template used to display the recent comments.
1 %ul.recent.comments
2 - recent_comments.each do |comment|
3 %li.comment
4 %h4= comment.created_at
5 = comment.text
6 .meta
7 Comment on:
8 = link_to comment.commentable.title, comment.commentable
9 # Yes, this would result in N+1 selects.
So far, so good. The polymorphic association makes it easy to access all types of comments in one listing. In order to find all of the unreviewed comments for an item, we can use a named scope on the Comment class together with the comments association.
1 classComment < ActiveRecord::Base
2 scope :unreviewed, -> { where(reviewed: false) }
3 end
4
5 >> timesheet.comments.unreviewed
Both Timesheet and ExpenseReport currently have identical has_many methods for comments. Essentially, they both share a common interface. They’re commentable!
To minimize duplication, we could specify common interfaces that share code in Ruby by including a module in each of those classes, where the module contains the code common to all implementations of the common interface. So, mostly for the sake of example, let’s go ahead and define aCommentable module to do just that, and include it in our model classes:
1 moduleCommentable
2 has_many :comments, as: :commentable
3 end
4
5 classTimesheet < ActiveRecord::Base
6 include Commentable
7 end
8
9 classExpenseReport < ActiveRecord::Base
10 include Commentable
11 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.
9.9.1 A Review of Class Scope and Contexts
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 classFoo < ActiveRecord::Base
2 has_many :bars
3 end
4 classFoo < ActiveRecord::Base
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 method 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:
1 moduleCommentable
2 has_many :comments, as: :commentable
3 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.
9.9.2 The included Callback
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):
1 moduleCommentable
2 defself.included(base)
3 base.class_eval do
4 has_many :comments, as: :commentable
5 end
6 end
7 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 technique is common enough, within Rails and gems, that it was added as a first-class concept in the ActiveSupport API as of Rails 3. The above example becomes shorter and easier to read as a result:
1 # app/models/concerns/commentable.rb
2 moduleCommentable
3 extend ActiveSupport::Concern
4 included do
5 has_many :comments, as: :commentable
6 end
7 end
Whatever is inside of the included block will get executed in the class context of the class where the module is included.
As of version 4.0, Rails includes the directory app/models/concerns as place to keep all your application’s model concerns. Any file found within this directory will automatically be part of the application load path.
Courtenay says…
There’s a fine balance to strike here. Magic like include Commentable certainly saves on typing and makes your model look less complex, but it can also mean that your association code is doing things you don’t know about. This can lead to confusion and hours of head-scratching while you track down code in a separate module. My personal preference is to leave all associations in the model, and extend them with a module. That way you can quickly get a list of all associations just by looking at the code.
9.10 Modifying Active Record Classes at Runtime
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 class31 of the model instance being loaded. Then we execute the contents of the configattribute 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.
Listing 9.5: Runtime metaprogramming with after_find
1 classAccount < ActiveRecord::Base
2 ...
3
4 protected
5
6 def after_find
7 singleton = class << self; self; end
8 singleton.class_eval(config)
9 end
10 end
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:
1 validates_numericality_of :product_code, only_integer: true
2 validates_length_of :product_code, is: 10
9.10.1 Considerations
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.
9.10.2 Ruby and Domain-Specific Languages
My former colleague Jay Fields and I pioneered the mix of Ruby metaprogramming, Rails, and internal32 domain-specific languages while doing Rails application development for clients. I still occasionally speak at conferences and blog about writing DSLs in Ruby.
Jay has also written and delivered talks about his evolution of Ruby DSL techniques, which he calls Business Natural Languages (or BNL for short33). 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.
Listing 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.
Courtenay says… DSLs suck! Except the ones written by Obie, of course. The only people who can read and write most DSLs are their original authors. As a developer taking over a project, it’s often quicker to just reimplement instead of learning the quirks and exactly which words you’re allowed to use in an existing DSL.In fact, a lot of Ruby metaprogramming sucks too. It’s common for people gifted with these new tools to go a bit overboard. I consider metaprogramming, self.included, class_eval, and friends to be a bit of a code smell on most projects.If you’re making a web application, future developers and maintainers of the project will appreciate your using simple, direct, granular, and well-tested methods, rather than monkeypatching into existing classes, or hiding associations in modules.That said, if you can pull it off… your code will become more powerful than you can possibly imagine. |
9.11 Using Value Objects
In Domain Driven Design34 (DDD) there is a distinction between Entity Objects and Value Objects. All model objects that inherit from ActiveRecord::Base could be considered Entity Objects in DDD. An Entity object cares about identity, since each one is unique. In Active Record, uniqueness is derived from the primary key. Comparing two different Entity Objects for equality should always return false, even if all of its attributes (other than the primary key) are equivalent.
Here is an example comparing two Active Record addresses:
>> home = Address.create(city: "Brooklyn", state: "NY")
>> office = Address.create(city: "Brooklyn", state: "NY")
>> home == office
=> false
In this case you are actually creating two new Address records and persisting them to the database, therefore they have different primary key values.
Value Objects on the other hand only care that all their attributes are equal. When creating Value Objects for use with Active Record you do not inherit from ActiveRecord::Base, but instead simply define a standard Ruby object. This is a form of composition, called an Aggregate in DDD. The attributes of the Value Object are stored in the database together with the parent object and the standard Ruby object provides a means to interact with those values in a more object oriented way.
A simple example is of a Person with a single Address. To model this using composition, first we need a Person model with fields for the Address. Create it with the following migration:
1 classCreatePeople < ActiveRecord::Migration
2 def change
3 create_table :people do |t|
4 t.string :name
5 t.string :address_city
6 t.string :address_state
7 end
8 end
9 end
The Person model looks like this:
1 classPerson < ActiveRecord::Base
2 def address
3 @address ||= Address.new(address_city, address_state)
4 end
5
6 def address=(address)
7 self[:address_city] = address.city
8 self[:address_state] = address.state
9
10 @address = address
11 end
12 end
We need a corresponding Address object which looks like this:
1 classAddress
2 attr_reader :city, :state
3
4 def initialize(city, state)
5 @city, @state = city, state
6 end
7
8 def ==(other_address)
9 city == other_address.city && state == other_address.state
10 end
11 end
Note that this is just a standard Ruby object that does not inherit from ActiveRecord::Base. We have defined reader methods for our attributes and are assigning them upon initialization. We also have to define our own == method for use in comparisons. Wrapping this all up we get the following usage:
>> gary = Person.create(name: "Gary")
>> gary.address_city = "Brooklyn"
>> gary.address_state = "NY"
>> gary.address
=> #<Address:0x007fcbfcce0188 @city="Brooklyn", @state="NY">
Alternately you can instantiate the address directly and assign it using the address accessor:
>> gary.address = Address.new("Brooklyn", "NY")
>> gary.address
=> #<Address:0x007fcbfa3b2e78 @city="Brooklyn", @state="NY">
9.11.1 Immutability
It’s also important to treat value objects as immutable. Don’t allow them to be changed after creation. Instead, create a new object instance with the new value instead. Active Record will not persist value objects that have been changed through means other than the writer method on the parent object.
9.11.1.1 The Money Gem
A common approach to using Value Objects is in conjunction with the money gem 35.
1 classExpense < ActiveRecord::Base
2 def cost
3 @cost ||= Money.new(cents || 0, currency || Money.default_currency)
4 end
5
6 def cost=(cost)
7 self[:cents] = cost.cents
8 self[:currency] = cost.currency.to_s
9
10 cost
11 end
12 end
Remember to add a migration with the 2 columns, the integer cents and the string currency that money needs.
1 classCreateExpenses < ActiveRecord::Migration
2 def change
3 create_table :expenses do |t|
4 t.integer :cents
5 t.string :currency
6 end
7 end
8 end
Now when asking for or setting the cost of an item would use a Money instance.
>> expense = Expense.create(cost: Money.new(1000, "USD"))
>> cost = expense.cost
>> cost.cents
=> 1000
>> expense.currency
=> "USD"
9.12 Non-Persisted Models
In Rails 3, if one wanted to use a standard Ruby object with Action View helpers, such as form_for, the object had to “act” like an Active Record instance. This involved including/extending various Active Model module mixins and implementing the method persisted?. At a minimum,ActiveModel::Conversion should be included and ActiveModel::Naming extended. These two modules alone provide the object all the methods it needs for Rails to determine partial paths, routes, and naming. Optionally, extending ActiveModel::Translation adds internationalization support to your object, while including ActiveModel::Validations allows for validations to be defined. All modules are covered in detail in the Active Model API Reference.
To illustrate, let’s assume we have a Contact class that has attributes for name, email, and message. The following implementation is Action Pack and Action View compatible in both Rails 3 and 4:
1 classContact
2 extend ActiveModel::Naming
3 extend ActiveModel::Translation
4 include ActiveModel::Conversion
5 include ActiveModel::Validations
6
7 attr_accessor :name, :email, :message
8
9 validates :name, presence: true
10 validates :email,
11 format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/ },
12 presence: true
13 validates :message, length: {maximum: 1000}, presence: true
14
15 def initialize(attributes = {})
16 attributes.each do |name, value|
17 send("#{name}=", value)
18 end
19 end
20
21 def persisted?
22 false
23 end
24 end
New to Rails 4 is the ActiveModel::Model, a module mixin that removes the drudgery of manually having to implement a compatible interface. It takes care of including/extending the modules mentioned above, defines an initializer to set all attributes on initialization, and sets persisted? tofalse by default. Using ActiveModel::Model, the Contact class can be implemented as follows:
1 classContact
2 include ActiveModel::Model
3
4 attr_accessor :name, :email, :message
5
6 validates :name, presence: true
7 validates :email,
8 format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/ },
9 presence: true
10 validates :message, length: {maximum: 1000}, presence: true
11 end
9.13 PostgreSQL enhancements
Out of all the supported databases available in Active Record, PostgreSQL received the most amount of attention during the development of Rails 4. In this section, we are going to look at the various additions made to the PostgreSQL database adapter.
9.13.1 Schema-less Data with hstore
The hstore data type from PostgreSQL allows for the storing of key/value pairs, or simply a hash, within a single column. In other words, if you are using PostgreSQL and Rails 4, you can now have schema-less data within your models.
To get started, first setup your PostgreSQL database to use the hstore extension via the enable_extension migration method:
1 classAddHstoreExtension < ActiveRecord::Migration
2 def change
3 enable_extension "hstore"
4 end
5 end
Next, add the hstore column type to a model. For the purpose of our examples, we will be using a Photo model with a hstore attribute properties.
1 classAddPropertiesToPhotos < ActiveRecord::Migration
2 change_table :photos do |t|
3 t.hstore :properties
4 end
5 end
With the hstore column properties setup, we are able to write a hash to the database:
1 photo = Photo.new
2 photo.properties # nil
3 photo.properties = { aperture: 'f/4.5', shutter_speed: '1/100 secs' }
4 photo.save && photo.reload
5 photo.properties # {:aperture=>"f/4.5", :shutter_speed=>"1/100 secs"}
Although this works well enough, Active Record does not keep track of any changes made to the properties attribute itself.
1 photo.properties[:taken] = Time.current
2 photo.properties
3 # {:aperture=>"f/4.5", :shutter_speed=>"1/100 secs",
4 # :taken=>Wed, 23 Oct 2013 16:03:35 UTC +00:00}
5
6 photo.save && photo.reload
7 photo.properties # {:aperture=>"f/4.5", :shutter_speed=>"1/100 secs"}
As with some other PostgreSQL column types, such as array and json, you must tell Active Record that a change has taken place via the <attribute>_will_change! method. However, a better solution is to use the Active Record store_accessor macro style method to add read/write accessors to hstore values.
1 classPhoto < ActiveRecord::Base
2 store_accessor :properties, :aperture, :shutter_speed
3 end
When we set new values to any of these accessors, Active Record is able to track the changes made to the underlying hash, eliminating the need to call the <attribute>_will_change! method. Like any accessor, they can have Active Model validations added to them and also be used in forms.
1 photo = Photo.new
2 photo.aperture = "f/4.5"
3 photo.shutter_speed = "1/100 secs"
4 photo.properties # {"aperture"=>"f/4.5", "shutter_speed"=>"1/100 secs"}
5
6 photo.save && photo.reload
7
8 photo.properties # {"aperture"=>"f/4.5", "shutter_speed"=>"1/100 secs"}
9 photo.aperture = "f/1.4"
10
11 photo.save && photo.reload
12 photo.properties # {"aperture"=>"f/1.4", "shutter_speed"=>"1/100 secs"}
Be aware that when a hstore attribute is returned from PostgreSQL, all key/values will be strings.
9.13.1.1 Querying hstore
To query against a hstore value in Active Record, use SQL string conditions with the where query method. For the sake of clarity, here are a couple examples of various queries that can be made against an hstore column type:
1 # Non-Indexed query to find all photos that have a key 'aperture' with a
2 # value of f/1.4
3 Photo.where("properties -> :key = :value", key: 'aperture', value: 'f/1.4')
4
5 # Indexed query to find all photos that have a key 'aperture' with a value
6 # of f/1.4
7 Photo.where("properties @> 'aperture=>f/1.4'")
8
9 # All photos that have a key 'aperture' in properties
10 Photo.where("properties ? :key", key: 'aperture')
11
12 # All photos that do not have a key 'aperture' in properties
13 Photo.where("not properties ? :key", key: 'aperture')
14
15 # All photos that contains all keys 'aperture' and 'shutter_speed'
16 Photo.where("properties ?& ARRAY[:keys]", keys: %w(aperture shutter_speed))
17
18 # All photos that contains any of the keys 'aperture' or 'shutter_speed'
19 Photo.where("properties ?| ARRAY[:keys]", keys: %w(aperture shutter_speed))
For more information on how to build hstore queries, you can consult the PostgreSQL documentation directly.36
9.13.1.2 GiST and GIN Indexes
If you are doing any queries on an hstore column type, be sure to add the appropriate index. When adding an index, you will have to decide to use either GIN or GiST index types. The distinguishing factor between the two index types is that GIN index lookups are three times faster than GiST indexes, however they also take three times longer to build.
You can define either a GIN or GiST index using Active Record migrations, by by setting the index option :using to :gin or :gist respectively.
add_index :photos, :properties, using: :gin
# or
add_index :photos, :properties, using: :gist
GIN and GiST indexes support queries with @>, ?, ?& and ?| operators.
9.13.2 Array Type
Another NoSQL like column type supported by PostgreSQL and Rails 4 is array. This allows us to store a collection of a data type, such as strings, within the database record itself. For instance, assuming we had a Article model, we could store all the article’s tags in an array attribute namedtags. Since the tags are not stored in another table, when Active Record retrieves an article from the database, it does so in a single query.
To declare a column as an array, pass true to the :array option for a column type such as string:
1 classAddTagsToArticles < ActiveRecord::Migration
2 def change
3 change_table :articles do |t|
4 t.string :tags, array: true
5 end
6 end
7 end
8 # ALTER TABLE "articles" ADD COLUMN "tags" character varying(255)[]
The array column type will also accept the option :length to limit the amount of items allowed in the array.
t.string :tags, array: true, length: 10
To set a default value for an array column, you must use the PostgreSQL array notation ({value}). Setting the default option to {} ensures that every row in the database will default to an empty array.
t.string :tags, array: true, default: '{rails,ruby}'
The migration in the above code sample would create an array of strings, that defaults every row in the database to have an array containing strings “rails” and “ruby”.
>> article = Article.create
(0.1ms) BEGIN
SQL (66.2ms) INSERT INTO "articles" ("created_at", "updated_at") VALUES
($1, $2) RETURNING "id" [["created_at", Wed, 23 Oct 2013 15:03:12
>> article.tags
=> ["rails", "ruby"]
Note that Active Record does not track destructive or in place changes to the Array instance.
1 article.tags.pop
2 article.tags # ["rails"]
3 article.save && article.reload
4 article.tags # ["rails", "ruby"]
To ensure changes are persisted, you must tell Active Record that the attribute has changed by calling <attribute>_will_change!.
1 article.tags.pop
2 article.tags # ["rails"]
3 article.tags_will_change!
4 article.save && article.reload
5 article.tags # ["rails"]
If the pg_array_parser gem is included in the application Gemfile, Rails will use it when parsing PostgreSQL’s array representation. The gem includes a native C extention and JRuby support.
9.13.2.1 Searching in Arrays
If you wish to query against an array column using Active Record, you must use PSQL’s methods ANY and ALL. To demonstrate, given our above example, using the ANY method we could query for any articles that have the tag “rails”:
Article.where("'rails' = ANY(tags)")
Alternatively, the ALL method searches for arrays where all values in the array equal the value specified.
Article.where("'rails' = ALL(tags)")
As with the hstore column type, if you are doing queries against an array column type, the column should be indexed with either GiST or GIN.
add_index :articles, :tags, using: 'gin'
9.13.3 Network Address Types
PostgreSQL comes with column types exclusively for IPv4, IPv6, and MAC addresses. IPv4 or IPv6 host address are represented with Active Record data types inet and cidr, where the former accepts values with nonzero bits to the right of the netmask. When Active Record retrievesinet/cidr data types from the database, it converts the values to IPAddr objects. MAC addresses are represented with the macaddr data type, which are represented as a string in Ruby.
To set a column as a network address in an Active Record migration, set the data type of the column to inet, cidr, or macaddr:
1 classCreateNetworkAddresses < ActiveRecord::Migration
2 def change
3 create_table :network_addresses do |t|
4 t.inet :inet_address
5 t.cidr :cidr_address
6 t.macaddr :mac_address
7 end
8 end
9 end
Setting an inet or cidr type to an invalid network address will result in an IPAddr::InvalidAddressError exception being raised. If an invalid MAC address is set, an error will occur at the database level resulting in an ActiveRecord::StatementInvalid: PG::InvalidTextRepresentationexception being raised.
>> address = NetworkAddress.new
=> #<NetworkAddress id: nil, inet_address: nil, ...>
>> address.inet_address = 'abc'
IPAddr::InvalidAddressError: invalid address
>> address.inet_address = "127.0.0.1"
=> "127.0.0.1"
>> address.inet_address
=> #<IPAddr: IPv4:127.0.0.1/255.255.255.255>
>> address.save && address.reload
=> #<NetworkAddress id: 1,
inet_address: #<IPAddr: IPv4:127.0.0.1/255.255.255.255>, ...>
9.13.4 UUID Type
The uuid column type represents a Universally Unique Identifier (UUID), a 128-bit value that is generated by an algorithm that makes it highly unlikely that the same value can be generated twice.
To set a column as a UUID in an Active Record migration, set the type of the column to uuid:
add_column :table_name, :unique_identifier, :uuid
When reading and writing to a UUID attribute, you will always be dealing with a Ruby string:
record.unique_identifier = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'
If an invalid UUID is set, an error will occur at the database level resulting in an ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation exception being raised.
9.13.5 Range Types
If you have ever needed to store a range of values, Active Record now supports PostgreSQL range types. These ranges can be created with both inclusive and exclusive bounds. The following range types are natively supported:
· daterange
· int4range
· int8range
· numrange
· tsrange
· tstzrange
To illustrate, consider a scheduling application that stores a date range representing the availability of a room.
1 classCreateRooms < ActiveRecord::Migration
2 def change
3 create_table :rooms do |t|
4 t.daterange :availability
5 end
6 end
7 end
8
9 room = Room.create(availability: Date.today..Float::INFINITY)
10 room.reload
11 room.availability # Tue, 22 Oct 2013...Infinity
12 room.availability.class # Range
Note that the Range class does not support exclusive lower bound. For more detailed information about the PostgreSQL range types, consult the official documentation37.
9.13.6 JSON Type
Introduced in PostgreSQL 9.2, the json column type adds the ability for PostgreSQL to store JSON structured data directly in the database. When an Active Record object has an attribute with the type of json, the the encoding/decoding of the JSON itself is handled behind the scenes byActiveSupport::JSON. This allows you to set the attribute to a hash or already encoded JSON string. If you attempt to set the JSON attribute to a string that cannot be decoded, a JSON::ParserError will be raised.
To set a column as JSON in an Active Record migration, set the data type of the column to json:
add_column :users, :preferences, :json
To demonstrate, let’s play with the preferences attribute from the above example in the console. To begin, I’ll create a user with the color preference of blue.
>> user = User.create(preferences: { color: "blue"} )
(0.2ms) BEGIN
SQL (1.1ms) INSERT INTO "users" ("preferences") VALUES ($1) RETURNING
"id" [["preferences", {:color=>"blue"}]]
(0.4ms) COMMIT
=> #<User id: 1, preferences: {:color=>"blue"}>
Next up, let’s verify when we retrieve the user from the database that the preferences attribute doesn’t return a JSON string, but a hash representation instead.
>> user.reload
User Load (10.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1
LIMIT 1 [["id", 1]]
=> #<User id: 1, preferences: {"color"=>"blue"}>
>> user.preferences.class
=> Hash
It’s important to note that like the array data type, Active Record does not track in place changes. This means that updating the existing hash does not persist the changes to the database. To ensure changes are persisted, you must call <attribute>_will_change! (preferences_will_change! in our above example) or completely replace the object instance with a new value instead.
9.14 Conclusion
With this chapter we conclude our coverage of Active Record. Among other things, we examined how callbacks let us factor our code in a clean and object-oriented fashion. We also expanded our modeling options by considering single-table inheritance, abstract classes and Active Record’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.