In the Depot application, LineItems have direct relationships to three other models: Cart, Order, and Product. Additionally, models can have indirect relationships mediated by resource objects. The relationship between Orders and Products through LineItems is an example of such a relationship.
All of this is made possible through IDs.
Active Record classes correspond to tables in a database. Instances of a class correspond to the individual rows in a database table. Calling Order.find(1), for instance, returns an instance of an Order class containing the data in the row with the primary key of 1.
If you’re creating a new schema for a Rails application, you’ll probably want to go with the flow and let it add the id primary key column to all your tables. However, if you need to work with an existing schema, Active Record gives you a way of overriding the default name of the primary key for a table.
For example, we may be working with an existing legacy schema that uses the ISBN as the primary key for the books table.
We specify this in our Active Record model using something like the following:
| class LegacyBook < ApplicationRecord |
| self.primary_key = "isbn" |
| end |
Normally, Active Record takes care of creating new primary key values for records that we create and add to the database—they’ll be ascending integers (possibly with some gaps in the sequence). However, if we override the primary key column’s name, we also take on the responsibility of setting the primary key to a unique value before we save a new row. Perhaps surprisingly, we still set an attribute called id to do this. As far as Active Record is concerned, the primary key attribute is always set using an attribute called id. The primary_key= declaration sets the name of the column to use in the table. In the following code, we use an attribute called id even though the primary key in the database is isbn:
| book = LegacyBook.new |
| book.id = "0-12345-6789" |
| book.title = "My Great American Novel" |
| book.save |
| # ... |
| book = LegacyBook.find("0-12345-6789") |
| puts book.title # => "My Great American Novel" |
| p book.attributes #=> {"isbn" =>"0-12345-6789", |
| # "title"=>"My Great American Novel"} |
Just to make life more confusing, the attributes of the model object have the column names isbn and title—id doesn’t appear. When you need to set the primary key, use id. At all other times, use the actual column name.
Model objects also redefine the Ruby id and hash methods to reference the model’s primary key. This means that model objects with valid IDs may be used as hash keys. It also means that unsaved model objects cannot reliably be used as hash keys (because they won’t yet have a valid ID).
One final note: Rails considers two model objects as equal (using ==) if they are instances of the same class and have the same primary key. This means that unsaved model objects may compare as equal even if they have different attribute data. If you find yourself comparing unsaved model objects (which is not a particularly frequent operation), you might need to override the == method.
As we will see, IDs also play an important role in relationships.
Active Record supports three types of relationship between tables: one-to-one, one-to-many, and many-to-many. You indicate these relationships by adding declarations to your models: has_one, has_many, belongs_to, and the wonderfully named has_and_belongs_to_many.
A one-to-one association (or, more accurately, a one-to-zero-or-one relationship) is implemented using a foreign key in one row in one table to reference at most a single row in another table. A one-to-one relationship might exist between orders and invoices: for each order there’s at most one invoice.
As the example shows, we declare this in Rails by adding a has_one declaration to the Order model and by adding a belongs_to declaration to the Invoice model.
There’s an important rule illustrated here: the model for the table that contains the foreign key always has the belongs_to declaration.
A one-to-many association allows you to represent a collection of objects. For example, an order might have any number of associated line items. In the database, all the line item rows for a particular order contain a foreign key column referring to that order.
In Active Record, the parent object (the one that logically contains a collection of child objects) uses has_many to declare its relationship to the child table, and the child table uses belongs_to to indicate its parent. In our example, class LineItem belongs_to :order, and the orders table has_many :line_items.
Note that, again, because the line item contains the foreign key, it has the belongs_to declaration.
Finally, we might categorize our products. A product can belong to many categories, and each category may contain multiple products. This is an example of a many-to-many relationship. It’s as if each side of the relationship contains a collection of items on the other side.
In Rails we can express this by adding the has_and_belongs_to_many declaration to both models.
Many-to-many associations are symmetrical—both of the joined tables declare their association with each other using “habtm.”
Rails implements many-to-many associations using an intermediate join table. This contains foreign key pairs linking the two target tables. Active Record assumes that this join table’s name is the concatenation of the two target table names in alphabetical order. In our example, we joined the table categories to the table products, so Active Record will look for a join table named categories_products.
We can also define join tables directly. In the Depot application, we defined a LineItems join, which joined Products to either Carts or Orders. Defining it ourselves also gave us a place to store an additional attribute, namely, a quantity.
Now that we have covered data definitions, the next thing you would naturally want to do is access the data contained within the database, so let’s do that.