7.7. Advanced ActiveRecord

Having covered most of what makes ActiveRecord an excellent ORM, you can move onto the next level, with more advanced topics; the knowledge of which will certainly come in handy more than a few times.

7.7.1. Single Table Inheritance

Relational databases work under the assumption that tables can be related through associations, as you have seen so far in this chapter. Object-oriented programming tends to use, where appropriate, inheritance, a concept that is foreign to the world of RDBMS. To bridge this gap, ActiveRecord allows you to easily implement single table inheritance.

Imagine that you want to represent photos, videos, and songs in your application. The traditional approach would be to use three tables (and three corresponding models):

create_table :songs do |t|
  t.string :name
  t.string :file_path
  t.integer :user_id
end

create_table :videos do |t|
  t.string :name
  t.string :file_path
  t.integer :user_id
end

create_table :photos do |t|
  t.string :name
  t.string :file_path
  t.integer :user_id
end

The problem with this approach is that it forces you to use three structurally identical tables to represent what are essentially just media files. Single table inheritance allows you to use a single common table for all three of them, whose corresponding model is the superclass of the three (Song, Video, and Photo) models.

The single table will be media_files:

create_table :media_files do |t|
  t.string :name
  t.string :file_path
  t.string :type
  t.integer :user_id
end

t.references or t.belongs_to instead of t.integer would work as well.

Notice that this has a special string column called type. This is used by ActiveRecord to distinguish a media file that happens to be a video from one that happens to be a photo or a song.

At this point, you'll have one parent model corresponding to that table and three subclasses:

class MediaFiles < ActiveRecord::Base
  belongs_to :user
end

class Song < MediaFiles
  # Some Song specific methods
end

class Video < MediaFiles
  # Some Video specific methods
end

class Photo < MediaFiles
  # Some Photo specific methods
end

This is very well organized from an object-oriented perspective and it uses a single table for all our media files. If you want to determine the class of a retrieved record, you can do so through the class method:

media_file = MediaFiles.find_by_name("Darkest Dreaming")
media_file.class # Song

Three important caveats apply:

  1. Don't try to access the type attribute directly, because type is also the name of a deprecated Ruby method. The far safer choice is to check the class through the class method and to automatically assign type a value by using a subclass of the main model (for example, Photo.create). If you really have to change the underlying value for an already defined object, use the hash notation. For example: media_file[:type] = Video.

  2. The superclass model can contain attributes that are defined only for certain subclasses. For instance, you could add a column called duration to the table media_files, which keeps track of the length of a song or a video. This attribute wouldn't apply to Photo though, so it would be important that such a column was defined as nullable.

  3. Unless you are using a table for single table inheritance, never add a column called type to it, because this will mislead ActiveRecord and result in all sorts of problems.

7.7.2. Polymorphic Associations

Polymorphic associations are a second option to simplify and improve the code quality when working with multiple models. They are very straightforward but tend to confuse newcomers. The prerequisite to avoid confusion is to understand the reason why you need them. Imagine that you have a one-to-many relationship between Post and Comment; the comments table will be akin to the following:

create_table :comments do |t|
  t.text :body
  t.string :author
  t.references :post_id
end

add_index :comments, :post_id

Notice that you need a foreign key to reference posts, and ideally, an index for it. Now, imagine that as you develop the application, you realize that you'd like to have the ability to add comments about companies, milestones, projects, and tasks. The comments table would have to include foreign keys for these as well:

create_table :comments do |t|
  t.text :body
  t.string :author
  t.references :post_id
  t.references :company_id
  t.references :milestone_id
  t.references :project_id
  t.references :task_id
end

add_index :comments, :post_id
add_index :comments, :company_id
add_index :comments, :milestone_id
add_index :comments, :project_id
add_index :comments, :task_id

Pretty ugly isn't it? What's worse is that in the database, you'll have records that look like the ones shown in the following table (which presents values in the comments table without polymorphic associations):

idbodyauthorpost_idcompany_idmilestone_idproject_idtask_id
1"...""Stan"NULL13NULLNULLNULL
2"...""Kyle"27NULLNULLNULLNULL
3"...""Eric"NULLNULLNULL3NULL
4"...""Kenny"NULLNULLNULLNULL42
5"...""Randy"NULLNULL5NULLNULL
6"...""Chef"27NULLNULLNULLNULL

This results in a table with many foreign keys, yet only one of them is actually used per record. When Kyle and Chef commented on a post, none of the foreign keys except for post_id were used to store integer values. When Stan commented on the company with id 13, the foreign keys post_id, milestone_id, project_id, and task_id were NULL. And so on for the remaining records.

The model Comment would have to be the following:

class Comment < ActiveRecord::Base
  belongs_to :post
  belongs_to :company
  belongs_to :milestone
  belongs_to :project
  belongs_to :task
end

Not really nice either!

Polymorphic associations allow you to DRY both the table definition and the resulting model by defining a common foreign key of choice, and a common type column that's named accordingly. The comments table would become:

create_table :comments do |t|
  t.text :body
  t.string :author
  t.integer :commentable_id
  t.integer :commentable_type
end

add_index :comments, [:commentable_id, :commentable_type]

NOTE

Always add an index for polymorphic tables. They tend to get large rather quickly and their performance can be severely impacted if indexes have not been defined.

Now the Comment model simply becomes:

class Comment < ActiveRecord::Base
  belongs_to :commentable, :polymorphic => true
end

You define a commentable association and specify that it's a polymorphic one through the :polymorphic option, so that ActiveRecord knows how to automatically handle commentable_id and commentable_type.

This will enable you to access the associated object through the method commentable. For example:

c = Comment.first
c.author                 # "Kenny"
c.commentable.class.to_s # "Task"
c.commentable.name       # "Great job with the migrations, Sean!"

The other five models will be able to access the comments for each of their objects, as long as they include has_many :comments, :as => commentable:

class Post < ActiveRecord::Base
  # ... some other associations ...
  has_many :comments, :as => commentable
end

class Company < ActiveRecord::Base
  # ... some other associations ...
  has_many :comments, :as => commentable
end

class Milestone < ActiveRecord::Base
  # ... some other associations ...
  has_many :comments, :as => commentable
end

class Project < ActiveRecord::Base
  # ... some other associations ...
  has_many :comments, :as => commentable
end

class Task < ActiveRecord::Base
  # ... some other associations ...
  has_many :comments, :as => commentable
end

That :as => commentable is required to specify a polymorphic interface.

For example:

Company.find_by_name("IBM").comments.each do |c|
  puts "Comment by #{c.author}: #{c.body}"
end

Behind the scenes, the second SELECT query (the one that retrieves the comments) issued ends with WHERE (comments.commentable_id = 22 AND comments.commentable_type = 'Company'), assuming that 22 is the id of the company IBM.

The table comments will also look slimmer and more compact as shown in the following table:

idbodyauthorcommentable_idcommentable_type
1"...""Stan"13"Company"
2"...""Kyle"27"Post"
3"...""Eric"3"Project"
4"...""Kenny"42"Task"
5"...""Randy"5"Milestone"
6"...""Chef"27"Post"

ActiveRecord automatically converts between the class and the corresponding string that's actually stored in the database.

Serializing

Generally speaking, it's possible to instruct ActiveRecord to store a Ruby object in a given column. The conversions from, and to, the database data type will be handled automatically then. To achieve this, you simply need to use the serialize :attribute_name method in the model definition. This is handier and safer than performing manual serialization, by converting an object into YAML format before an INSERT (with the to_yaml method), and then converting it back manually when you need to retrieve the object (for example, with YAML::load).


Notice that commentable is an arbitrarily chosen word and could be replaced with any non-reserved word of your choice. You just have to be consistent in using it for the foreign key, for the *_type column, and in the model definitions.

The example I've used here lends itself to explain why polymorphic associations are a very useful feature. That said, to implement comments the polymorphic way in your projects, you can probably save some time and code by employing the acts_as_commentable plugin instead.

7.7.3. Callbacks

Though ActiveRecord is happy to handle the life cycle of objects on its own, it also provides you with a series of special methods, called callbacks, that allow you to intervene and decide that certain actions should be taken before or after an object is created, updated, destroyed, and so on.

Callback methods for validations are:

  • before_validation

  • before_validation_on_create

  • before_validation_on_update

  • after_validation

  • after_validation_on_update

  • after_validation_on_create

Their names are quite self-explanatory. The before_* callbacks are triggered before a validation, and the after_* callbacks are used to execute code afterwards. The *_on_update and *_on_create callbacks are only triggered by validations for update and create operations, respectively. before_validation and after_validation apply to both updates and creations.

Callbacks specific to the creation of an object are:

  • before_create

  • after_create

Likewise, callbacks triggered by the update of a record only are:

  • before_update

  • after_update

The following two callbacks are executed for both updates and inserts:

  • before_save

  • after_save

Callbacks specific to destroy are:

  • before_destroy

  • after_destroy

There are then two after_* callbacks, without a before version:

  • after_find

  • after_initialize

Note that for each call to save, create, or destroy, there is an ordered chain of callbacks that allows you to intervene with the execution of your custom code in the exact spot that you desire. For example, for the creation of a new record the callback order is as follows:

  1. before_validation

  2. before_validation_on_create

  3. after_validation

  4. after_validation_on_create

  5. before_save

  6. before_create

  7. after_create

  8. after_save

Between 2 and 3, the validation occurs, and between 6 and 7, the actual INSERT is issued.

To execute code during a callback, you'll need to use any of these methods in the model definition. Three options are available.

You can define the callback method within the model:

class User < ActiveRecord::Base
  # ...

  def before_save
    # Some code to encrypt the password
    # ...
  end
end

Or, in a nicer way, you can pass a symbol representing the handler method:

class User < ActiveRecord::Base
  # ...
  before_save :encrypt_password

  private

  def encrypt_password
    # Some code to encrypt the password
    # ...
  end
end

The method encrypt_password will be always executed before a user is created or updated.

You could even create a class that defines several callback methods so that they can be shared among several models. To do so, you'll just need to pass an object to the callback method within the model required (for example, before_save MyClass.new, but first require "myclass" in the model's file).

The third way to define code that's to be executed during a callback is to use a block:

class User < ActiveRecord::Base
  # ...
  before_save do |user|
    # Some code to encrypt the password

# user is the instance that will be saved
  end
end

7.7.4. Association Callbacks

Regular callbacks allow you to hook into the life cycle of an ActiveRecord object, but what about an association collection? It turns out that ActiveRecord allows you to execute custom code before and after an object is added or removed from an association collection. The callback options are :before_add, :after_add, :before_remove, and :after_remove. For example:

class Company < ActiveRecord::Base
  has_many :employees, :after_add => :enable_badge, :after_remove => :disable_badge

  def enable_badge(employee)
    # ...
  end

  def disable_badge(employee)
    # ...
  end
end

NOTE

Just as with regular callbacks, if an exception is raised during the execution of a :before_add callback, the object will not be added to the collection. Likewise, if an exception is raised during the execution of the handler for the :before_remove callback, the object will not be removed from the collection.

7.7.5. Observers

As previously mentioned, it's possible to share the callbacks that are defined within a class with several models. To do this you first define a class with a few callbacks:

class MyCallbackObject

  def initialize(list_of_attributes)
    # ... initialize here...
  end
  def before_save
    # ... your logic here ...
  end

  def after_save
    # ... your logic here ...
  end
end

And then, include them in each model that requires those callbacks:

class MyModel < ActiveRecord::Base

cb = MyCallbackObject.new([:attr1, :attr2, :attr3])

  before_save cb
  after_save cb
end

Obviously replace all the generic names with the ones that apply. For example, instead of attr1, use the name of the real attribute that is required by the callback object. Check the online documentation for ActiveRecord::Callbacks for more examples and details.

NOTE

At the time of this writing, when you define an after_find or after_initialize within a callback object, these will need to appear in the model as empty methods as well, because otherwise ActiveRecord won't execute them.

The main downside of using callback objects is that they clutter the model, often with a series of functionalities, like logging, which are not really the model's responsibility. That clutter is then repeated over and over for each model that requires the callbacks that have been defined by the callback object.

The Single Responsibility Principle has led ActiveRecord's developers to include a powerful and elegant feature that solves this problem: observers. Observer classes are used to access the model's life cycle in a trigger-like fashion without altering the code of the model that's being observed.

Observers are subclasses of ActiveRecord::Observer are by convention named after the class they observe, to which the Observer token is appended:

class UserObserver < ActiveRecord::Observer
  def after_save(user)
    user.logger.info("User created: #{user.inspect}")
  end
end

Conventionally, observers are stored in appmodels just like models or callback objects.

The problem with this convention is that observers are often used to "observe" many models, so it's usually necessary to overwrite the convention through the observe method:

class Auditor < ActiveRecord::Observer
  observe User, Account

  def after_save(model)
    model.logger.info("#{model.class} created: #{model.inspect}")
  end
end

Without having to touch the code of User or Account, the preceding code will log the creation of both users and accounts, as long as you enable the observer in your Rails application. But first you'll need to add the following line in your configenvironment.rb file, where a commented, similar line already exists:

config.active_record.observers = :auditor

If you've created more than one observer, you can assign a comma-separated list of symbols to observers.

If, on the other hand, you are not using Rails, you can load observers through their instance method: ModelObserver.instance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.111.197