7.5. ActiveRecord Associations

Associations are highly readable class methods used to define relationships between models, allowing you to easily work with associated objects. The convenience of Book.find_by_title("On the Road").author.name is only possible thanks to the fact that you established a relationship between the Book and Author class models through a pair of association methods. By employing them (has_many and belongs_to), and respecting the foreign key naming convention by adding an author_id column to the books table, you automatically obtained several methods that are added to instances of both models, including author, which returns the Author object for any given Book object.

The association methods are:

  • belongs_to

  • has_one

  • has_many

  • has_and_belongs_to_many

These ActiveRecord associations are used to define three types of relationships between models: one-to-one, one-to-many, and many-to-many. These are covered in the next few sections.

7.5.1. One-to-one Relationships

One-to-one relationships are relationships in which one row in a certain table references one row (at most) of the related table; so it could also reference no rows. A one-to-one relationship between models can be established by employing the has_one and belongs_to association methods. It is common for newcomers to confuse which model should have which of the two methods in their definition. The rule is rather clear though: use belongs_to in the definition of the model whose corresponding table has the foreign key.

The following example establishes a one-to-one relationship between an Account model and an Employee model:

class Account < ActiveRecord::Base
  belongs_to :employee  # foreign key: employee_id
end

class Employee < ActiveRecord::Base
  has_one :account
end

Note that following the ActiveRecord convention for foreign key fields, as the rule described earlier stated, the table accounts needs to have an employee_id foreign key that references the primary key of the employees table.

7.5.2. One-to-many Relationships

One-to-many relationships are relationships in which one row in a certain table references an arbitrary number of rows in the related table. One-to-many relationships between models are established through the has_many and belongs_to methods. Imagine that you have two models, Product and Company. It's reasonable to assume that "a company has many products, and a product belongs to a company," right? Let's translate this into code:

class Company < ActiveRecord::Base
  has_many :products
end


class Product < ActiveRecord::Base
  belongs_to :company  # foreign key: company_id
end

That was rather effortless, and is pretty much what you did in the previous chapter with articles and comments. Notice that you passed a symbol to the has_many method (that is, :products), which represents a collection, and as such, the plural form was employed. As usual, the model whose definition contains the belongs_to call is the one whose corresponding table has the foreign key. In this case, this is company_id in the table products.

7.5.3. Many-to-many Relationships

Many-to-many relationships are relationships in which an arbitrary number of rows in a table reference an arbitrary number of rows in another table. This type of relationship between models can be established in two different ways.

7.5.3.1. Using a Join Table

The first approach requires a join table that's been named according to the convention, which is going to contain two foreign keys, one for each of the referenced tables. This table is not supposed to have an id, because the foreign key pair uniquely identifies each record.

This join table doesn't have a model that represents it, and its name should be created by joining the two table names in alphabetical order, separating them with an underscore. When this approach is taken, the verbose method has_and_belongs_to_many (aka, habtm) is used in both models to specify the many-to-many relationship.

If you take into consideration a Post and a Category model, it isn't unreasonable to state that "a post has and belongs to many categories" and of course, vice versa, "a category has and belongs to many posts." This translates directly into this code:

class Post < ActiveRecord::Base
  has_and_belongs_to_many :categories
end

class Category < ActiveRecord::Base
  has_and_belongs_to_many :posts
end

Note that in a real project, a model may have several types of relationships with other models. Because of this, it's common to see several association methods within the definition of a model.

Once again, the principle of Convention over Configuration makes our life easy. The join table will need to be called categories_posts, unless you specify otherwise, and will only contain a category_id and post_id foreign key column. No foreign keys for this relationship should be included in either the categories or posts tables.

The join table doesn't have a representing model, but you may still want to create it through migrations. The peculiarity of this table is that it has no id, so the code within the migration will need to look like this:

create_table :categories_posts, :id => false do |t|
  t.integer :category_id
  t.integer :post_id
end

You can add an optional :null => false parameter to each column when you want to prevent categories that don't reference any posts, and posts that don't reference any categories.

Specifying :id => false as the second argument of the create_table method is enough to prevent the table from having the default id primary key. Also note that join tables are notorious for quickly growing large in size; for this reason it's highly recommended that you add a couple of indexes to the migration, which will also help to maintain acceptable performances:

add_index(:categories_posts, [:category_id, :post_id], :unique => true)
add_index(:categories_posts, :post_id, :unique => false)

The first composite index speeds up searches on the foreign key pair and (usually) for looking up category_id as well. The second index speeds up lookups for post_id. You pass :unique => true to the first add_index call because each pair containing category_id and post_id is unique.

7.5.3.2. Using a Join Model

The second approach when it comes to building many-to-many relationships employs an intermediary model. This will act as your join table, and unlike the preceding approach, the corresponding table can contain other columns for fields you'd like to track.

Imagine wanting to create a many-to-many relationship between the model Actor and the model Movie. An actor "has" many movies, and a movie has many actors. Instead of proceeding as you did before with a join table, you'll introduce a third "join" model, Appearance. The corresponding table appearances will contain the foreign key columns to reference actors and movies, as well as other fields that you are interested in. For example, you may wish to store and retrieve the fictional character (for example, Austin Powers), the role (for example, Protagonist), and whether the actor/actress received an Oscar for their performance (a Boolean will do). The migration file for such a table might look like this:

create_table :appearances, :id => false do |t|
  t.integer :actor_id
  t.integer :movie_id
  t.string :character

t.string :role
  t.boolean :oscar
end

The models will then be:

class Appearance < ActiveRecord::Base
  belongs_to :actor
  belongs_to :movie
end

class Actor < ActiveRecord::Base
  has_many :appearances
  has_many :movies, :through => :appearances
end

class Movie < ActiveRecord::Base
  has_many :appearances
  has_many :actors, :through => :appearances
end

appearances is the table with the foreign keys, so the belongs_to method is found in the corresponding Appearance model. Also, both actors and movies have many appearances, so you need to specify that point with has_many :appearances in both the Actor and Movie model.

The key point, and the new element here, is the :through parameter. That parameter allows you to specify a collection, a plural form of the join model (that is, Appearance) that enables the relationship to exist. In other words, you need to specify that "a movie has many actors through appearances" and that "an actor has many movies through appearances."

7.5.4. Auto-generated Methods

Declaring associations is important because it greatly simplifies the process of working with tables that are related. Instead of providing custom queries or requiring multiple steps to retrieve the information you need from a related table, associations allow you to work with linked objects by automatically adding new methods to the model class.

Every time you add has_one, belongs_to, has_many, or has_and_belongs_to_many to a model, you are also automatically adding a series of instance methods to that model, so that you can easily retrieve, create, update, and delete related/linked objects. You already saw this in the previous chapter, when you were able to retrieve a list of comments for a given article by simply invoking the method comments, but dig a little deeper to see how useful associations really are.

Consider the one-to-many example shown earlier. Imagine that you have found a certain product based on the id that the user specified:

product = Product.find(params[:id])

If associations didn't exist, and you wanted to retrieve the name of the company that produced a particular product, you'd have to specify your own SQL query or do something like this:

company_id = product.company_id
company_name = Company.find(company_id).name

Thanks to belongs_to :company within the model definition, the instance method company was added for you, so you can do this in one straightforward step:

company_name = product.company.name

Similarly, if you wanted to retrieve a list of products for a given company, you could use the products method added by has_many :products:

Company.find(551).products

NOTE

Because the instance methods added to the model are based on the name of the association, it's important that you don't create associations with the same name as any pre-existing ActiveRecord::Base instance methods. If you do, the generated methods will overwrite the existing ones, thus breaking your application. The official API documentation warns against names such as attributes and connection, but don't be afraid to double-check the existing instance methods in the documentation and read over this — somewhat in need of a cleanup — Wiki page: http://wiki.rubyonrails.org/rails/pages/ReservedWords.

Depending on the macro-like method employed, different instance methods are automatically defined for you. Let's check them out.

7.5.4.1. belongs_to

The belongs_to method adds a series of methods to deal with a single associated object. The names of the generated methods are determined by the symbol that's passed as the first parameter to the method. To keep things generic enough we'll use — just as the documentation does — the generic term association to identify the name of the associated object:

  • association(force_reload = false): Returns the associated object if it exists. If it doesn't, nil is returned. The result is cached, so pass true if you'd like to force the method to actually hit the database.

  • association=(associate): Assigns an associate object by assigning the primary key of associate to the foreign key (for example, associate_id) of the receiver.

  • association.nil?: Answers the question, "Are there no associated objects?"

  • build_association(attributes = {}): Returns a new associated object that's instantiated with the attributes that were passed to the method. The object is instantiated but hasn't been saved in the database yet.

  • create_association(attributes = {}): Does the same thing as build_association, but it actually saves the associated object (assuming that the existing validations are passed).

Let's see what this means in practice. In the relationships described before, the following models employed the belongs_to method:

  1. Account: in a one-to-one relationship with Employee (that is, belongs_to :employee).

  2. Product: in a one-to-many relationship with Company (that is, belongs_to :company).

  3. Appearance: join model in a many-to-many relationship between Movie and Actor (that is, belongs_to :movie and belongs_to :actor).

Account objects will have these new methods: employee, employee=, employee.nil?, build_employee, and create_employee.

Likewise, Product objects will have these methods added: company, company=, company.nil?, build_company, and create_company.

Not surprisingly, Appearance objects will be able to access the linked Movie and Actor objects with the following methods: movie, movie=, movie.nil?, build_movie, create_movie, and actor, actor=, actor.nil?, build_actor, and create_actor.

To illustrate the usage of these methods, let's consider Product and Company (assuming that you started with a database without any records in it):

>> Product.all
=> []

You can create a new product as usual through the create method:

>> product = Product.create(:name => "Time-travel machine", :copies => 3)
=> #<Product id: 1, name: "Time-travel machine", copies: 3, company_id: nil, cre
ated_at: "2008-08-25 22:41:33", updated_at: "2008-08-25 22:41:33">

Let's put the company and company.nil? methods we talked about to good use:

>> product.company
=> nil
>> product.company.nil?
=> true

product is just a variable that contains a reference to the Product object. It could be named arbitrarily.

The company is nil because the company_id attribute for product is nil. Had company_id stored the value of the primary key of an existing company, product.company would have returned that company object and product.company.nil? would have been false.

You can change this by linking the product to a company. In our hypothetical scenario, you don't have a company yet, so you'll have to create one. The easiest way to do that is to use the create_company method:

>> product.create_company(:name => "Back to the future Inc.")
=> #<Company id: 1, name: "Back to the future Inc.", address: nil, city: nil,
 country: nil, postal_code: nil, phone: nil, created_at: "2008-08-25 22:57:11",
 updated_at: "2008-08-25 22:57:11">

Not all the attributes for the company were assigned a value, because I only specified the name. This is okay, as long as those fields are nullable.

This has created a new company (with id 1) and automatically linked this object to the product object. You can verify this by running:

>> product.company
=> #<Company id: 1, name: "Back to the future Inc.", address: nil, city: nil,
country: nil, postal_code: nil, phone: nil, created_at: "2008-08-25 22:57:11",
 updated_at: "2008-08-25 22:57:11">

Now you can conveniently access the company that a product belongs to. Because the company method returns a Company instance, you can access its attributes as well:

>> product.company.name
=> "Back to the future Inc."

The link between the product and the company object is created through create_company, by assigning the id of the company object to the company_id attribute of the product object. You can verify this by running:

>> product.company_id
=> 1

Notice that if you want to assign an existing company to a given product, you can easily do so through the company= writer method:

product.company = another_company
product.save

product.save is required to actually store this change in the database.

Assuming that another_company is a Company object.

product.build_company

You could have used product.build_company instead of product.create_company. It's important to understand the differences though.

Using build_company would have instantiated a new company object and linked it to the product object. However, the company object would not have been saved in the database yet.

Furthermore, it's true that product.company would return this new object, but the id of this new company would be nil because the object hasn't been saved yet. As a direct consequence of this, the product's attribute, company_id, would not have been changed either.

A save call is required to save the new company in the database. Invoking product.company.save would save the company object in the database, but fail to update the foreign key company_id in the product, with the newly generated company id. To save the company in the database and permanently assign it to the product, just like create_company automatically did for you, you'd have to use product.save. This would store the new company in the table and set its id as the value of the product's company_id attribute.


belongs_to accepts several options. :class_name is used to specify a class name that is different from the name of the association. For example, belongs_to :perfomer, :class_name => "Actor" will create a performer association that uses the Actor model.

By convention, ActiveRecord assumes that the foreign key will be the name of the association, plus _id, :foreign_key can be used to specify a foreign key with a different name.

:class_name and :foreign_key are often employed when defining self-referential relationships. These are relationships in which rows of a table can refer to rows of that same table through a foreign key column.

:conditions, :select, :include, and :readonly are also available, and they are essentially the usual options you've seen before when describing finders.

NOTE

When using :select to define an association, always include the primary key and the foreign keys or you'll run into trouble.

If you check the online documentation you'll notice that there are three extra options :polymorphic, :counter_cache, and :dependent. The first is used for polymorphic associations, a concept that's explained later on in the chapter, and :counter_cache is used for counter cache columns, a subject you'll deal with in Chapter 11, when covering performances and optimization.

The third one, :dependent, allows you to specify a business rule when it comes to associated objects. If :dependent is not specified, when you drop an object you won't automatically drop the associated object. The :dependent option can accept :destroy as a value to specify that the associated object (for example, an employee) should be destroyed when you invoke destroy on the object (for example, an account). This key can also be set to :delete, in which case the associated object is deleted without invoking the destroy method (do you remember the difference between delete and destroy?).

For example, you can modify the definition of Account in the scenario indicated in the one-to-one relationship section to include :dependent:

class Account < ActiveRecord::Base
  belongs_to :employee, :dependent => :destroy
end

If you now destroy an Account object, the associated Employee object is also destroyed. The :dependent option allows you to enforce referential integrity directly from your application code.

It's important to be careful with this option. For example, if you drop a product, what will happen to the associated company? Should you drop it as well? The answer is simply, no, because a company may have many other products. You don't want to have "orphan" products whose parent company has just been destroyed. For this reason, never specify the :dependent option for a belongs_to when this is used in conjunction with has_many to establish a relationship.

For all intents and purposes, this option is only used when establishing a relationship where a has_one has been used in the associated class. In such a one-to-one relationship, like the employee/account example above, it's reasonable to assume that there are times when the :dependent => :destroy option may be required. When you destroy an order, you may want to destroy the associated invoice; when you destroy a user, you may want to destroy the associated profile, and so on.

7.5.4.2. has_one

belongs_to added several methods to operate on what can be considered the "parent" object. has_one sits at the opposite side of a one-to-one relationship, and as such adds a series of methods to operate on the "child" object.

The terms "parent object" and "child object" are loosely used to indicate an object that represents a row in the parent table and in the child table. In this context, parent/child has nothing to do with class inheritance.

As a matter of fact, the methods you saw before still apply: association, association=, association.nil?, build_association, and create_association (again, replace association with the real name of the association at hand). Sticking to the Employee and Account example, an employee object automatically has the following methods added to it: account, account=, account.nil?, build_account, and create_account.

At this point, you may wonder why you need a different macro-like method to obtain the same methods in the end. The reality is that there are a few important differences between them, including but not limited to, the many other parameter options available for has_one.

Unlike belongs_to, with has_one, assigning an associated object automatically saves that object and the object that is being replaced (if it exists), unless the parent/receiver object is a new record (verifiable through new_record?). For example, consider the following:

employee.account = an_unsaved_account

Unless employee is a new record, this will save the newly created object an_unsaved_account, assigning the id of employee to the attribute employee_id of an_unsaved_account. It will also save the existing employee.account unless it was nil, so that its employee_id can be updated to nil, therefore breaking the link with the employee object and making it an "orphan."

As a reminder, when an attribute is assigned a nil value, and then the record is saved, the value that's actually stored in the row is NULL.

Among the additional options are :order to specify the order in which the associated object will be picked through an SQL fragment string; :as for polymorphic associations, and :through, :source, and :source_type to work with a join model.

For details and examples of the many options available, always check the up-to-date online documentation. For these methods, check the documentation for module ActiveRecord::Associations::ClassMethods.

The :dependent option for has_one indicates to ActiveRecord what it's supposed to do when you destroy a parent object. :destroy or true destroys the corresponding row in the child table, whereas :nullify turns it into an orphan by assigning NULL to the foreign key, and :false or nil tells ActiveRecord not to update the child object or row.

7.5.4.3. has_many and habtm

The has_many and has_and_belongs_to_many methods both add a series of methods that are used to operate on a collection of associated objects. Using the generic name collection, the methods automatically added to the model instances are:

  • collection(force_reload = false): Returns an array of all the associated objects or an empty array if none are found. Pass true to bypass caching and force the method to query the database.

  • collection<<(object, ...): Adds one or more objects to the collection, assigning their foreign key to the primary key of the parent object. Unless the parent is a new record, adding objects to a collection automatically saves them in the database.

  • collection.delete(object, ...): Removes one or more objects from the collection by assigning their foreign key to NULL. In a one-to-many relationship (where an object's model defines a belongs_to, referring to the parent's model), the objects will also be destroyed if so indicated through a :dependent option. However, this is not the case if the association was created with a :through option.

  • collection=objects: Removes the existing objects and adds any new ones that are being assigned to the collection.

  • collection_singular_ids: Returns an array of ids for the associated objects in the collection.

  • collection_singular_ids=ids: Similar to collection=, it replaces the collection by identifying the new objects that are to be assigned through their ids.

  • collection.clear: By default this method removes all the objects from the collection by assigning the value NULL to their foreign key. When a :dependent => :destroy has been specified though, it destroys the records. Likewise, :dependent => :delete_all deletes them directly from the database without invoking destroy.

  • collection.empty?: Returns true if there are no associated objects, and false if at least one object belongs to the collection.

  • collection.size: Returns the number of associated objects.

  • collection.find(*args): Finds an associated object by following the same rules of the ActiveRecord::Base.find method.

  • collection.build(attributes = {}, ...): Returns one or more new objects of the collection type, instantiated with the attributes passed to the method. Each object is linked to the parent but it has yet to be saved.

  • collection.create(attributes = {}): Similar to build, but it actually saves the objects.

ActiveRecord's development, which spanned several years, implied that there were, and still are, other similar methods available, each with slight differences from the ones mentioned here. For example, there is size, but also length and count. There is clear, but also delete_all and destroy_all. Knowing the handful of methods described here should be more than enough to master most situations. The official documentation is a great place to check which methods are available, as well as the subtle differences between them.

The generated method here (generically called collection) is an array, so other regular Ruby methods will be available to work with it as well.

Taking into consideration the one-to-many relationship between the Company and Product models, has_many adds all the methods mentioned previously to Company objects: products, products<<, products.delete, products=, products_singular_ids, products_singular_ids=, products.empty?, products.size, products.find, products.build, and products.create.

In the many-to-many example, where the :through option has been employed, both Movie and Actor objects have methods that access each other's collections, as well as the appearances' rows. So you may invoke a_movie.actors, an_actor.movies, or even a_movie.appearances and an_actor.appearances.

Many options are available for the has_many method: :class_name, :conditions, :order, :foreign_key, :include, :group, :limit, :offset, :select, :as, :through, :source, :source_type, :uniq (to omit duplicates), :readonly, :dependent, :finder_sql (to specify a full SQL statement), :counter_sql (to specify a full SQL statement to retrieve the size of the collection), and :extend.

:dependent admits :destroy, which destroys all the associated objects by calling their destroy method; :delete_all, which deletes all the rows from the table without calling the destroy method; and :nullify, which sets the foreign key to NULL for all the associated objects without calling their save callbacks. You'll dig deeper into the issue of callbacks later on in this chapter.

NOTE

As an exercise, feel free to go ahead and modify the Article model in the blog application you defined in Chapters 5 and 6, so that when an article is destroyed, all of its comments are deleted (for example, use :dependent => :delete_all).

:extend is used to specify a named model that extends the proxy. The whole concept is covered in the next section.

The method has_and_belongs_to_many doesn't have a :dependent option, and as such, collection.clear doesn't destroy any objects from the join table. Options are also available that are specific to habtm: :join_table (to specify a custom table name), :association_foreign_key, :delete_sql, and :insert_sql. Please check the online documentation for details and examples.

7.5.5. Association Extensions

When you define an association, you can pass a block to it, in which it's possible to define methods that are only available from the association.

For example, you can define the method best_sellers for the products association collection:

class Company < ActiveRecord::Base
  has_many :products do
    def best_sellers(limit)
      find(:all, :limit => limit, :order => "sales DESC")
    end
  end
end

Eager Loading Associations

Earlier in the chapter I mentioned how it's dangerous to use conditions in conjunction with a hierarchy of :include due to the resulting complex (and slow) SQL. I mentioned that "it is common to eager load an association that has conditions defined on it." That sentence was rather cryptic at the time, but now that you have gained knowledge of associations and their options, I can provide you with an example (from the official documentation).

Rather than doing this:

Post.find(:all, :include => [ :author, :comments ],
:conditions =>
['comments.approved = ?', true])

You can define an association with a condition:

class Post < ActiveRecord::Base
  has_many :approved_comments, :class_name => 'Comment',
:conditions =>
 ['approved = ?', true]
end

And then eager load the association, which contains only your approved comments, with :include:

Post.find(:all, :include => :approved_comments)


You can then use it as a regular method of the collection:

Company.find(5).products.best_sellers(10)

Associations can also be extended by adding all the methods you want to define into a module, and then using the :extend option to add them to the association as class methods:

class Company < ActiveRecord::Base
  has_many :products, :extend => SalesFinder
end

Assign an array of modules to :extend, if you need to include the methods from several modules.

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

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