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.
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:
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.
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.
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.
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):
id | body | author | post_id | company_id | milestone_id | project_id | task_id |
---|---|---|---|---|---|---|---|
1 | "..." | "Stan" | NULL | 13 | NULL | NULL | NULL |
2 | "..." | "Kyle" | 27 | NULL | NULL | NULL | NULL |
3 | "..." | "Eric" | NULL | NULL | NULL | 3 | NULL |
4 | "..." | "Kenny" | NULL | NULL | NULL | NULL | 42 |
5 | "..." | "Randy" | NULL | NULL | 5 | NULL | NULL |
6 | "..." | "Chef" | 27 | NULL | NULL | NULL | NULL |
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:
id | body | author | commentable_id | commentable_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.
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.
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:
before_validation
before_validation_on_create
after_validation_on_create
before_save
before_create
after_create
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
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.
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.
18.227.111.197