- •Contents
- •Preface to the Second Edition
- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •The Architecture of Rails Applications
- •Models, Views, and Controllers
- •Active Record: Rails Model Support
- •Action Pack: The View and Controller
- •Installing Rails
- •Your Shopping List
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Linux
- •Development Environments
- •Rails and Databases
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A3: Validate!
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B4: Linking to the Cart
- •Task C: Cart Creation
- •Sessions
- •Iteration C1: Creating a Cart
- •Iteration C2: A Smarter Cart
- •Iteration C3: Handling Errors
- •Iteration C4: Finishing the Cart
- •Task D: Add a Dash of AJAX
- •Iteration D1: Moving the Cart
- •Iteration D3: Highlighting Changes
- •Iteration D4: Hide an Empty Cart
- •Iteration D5: Degrading If Javascript Is Disabled
- •What We Just Did
- •Task E: Check Out!
- •Iteration E1: Capturing an Order
- •Task F: Administration
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Iteration F4: A Sidebar, More Administration
- •Task G: One Last Wafer-Thin Change
- •Generating the XML Feed
- •Finishing Up
- •Task T: Testing
- •Tests Baked Right In
- •Unit Testing of Models
- •Functional Testing of Controllers
- •Integration Testing of Applications
- •Performance Testing
- •Using Mock Objects
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Logging in Rails
- •Debugging Hints
- •Active Support
- •Generally Available Extensions
- •Enumerations and Arrays
- •String Extensions
- •Extensions to Numbers
- •Time and Date Extensions
- •An Extension to Ruby Symbols
- •with_options
- •Unicode Support
- •Migrations
- •Creating and Running Migrations
- •Anatomy of a Migration
- •Managing Tables
- •Data Migrations
- •Advanced Migrations
- •When Migrations Go Bad
- •Schema Manipulation Outside Migrations
- •Managing Migrations
- •Tables and Classes
- •Columns and Attributes
- •Primary Keys and IDs
- •Connecting to the Database
- •Aggregation and Structured Data
- •Miscellany
- •Creating Foreign Keys
- •Specifying Relationships in Models
- •belongs_to and has_xxx Declarations
- •Joining to Multiple Tables
- •Acts As
- •When Things Get Saved
- •Preloading Child Rows
- •Counters
- •Validation
- •Callbacks
- •Advanced Attributes
- •Transactions
- •Action Controller: Routing and URLs
- •The Basics
- •Routing Requests
- •Action Controller and Rails
- •Action Methods
- •Cookies and Sessions
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Using Helpers
- •How Forms Work
- •Forms That Wrap Model Objects
- •Custom Form Builders
- •Working with Nonmodel Fields
- •Uploading Files to Rails Applications
- •Layouts and Components
- •Caching, Part Two
- •Adding New Templating Systems
- •Prototype
- •Script.aculo.us
- •RJS Templates
- •Conclusion
- •Action Mailer
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Secure and Deploy Your Application
- •Securing Your Rails Application
- •SQL Injection
- •Creating Records Directly from Form Parameters
- •Avoid Session Fixation Attacks
- •File Uploads
- •Use SSL to Transmit Sensitive Information
- •Knowing That It Works
- •Deployment and Production
- •Starting Early
- •How a Production Server Works
- •Repeatable Deployments with Capistrano
- •Setting Up a Deployment Environment
- •Checking Up on a Deployed Application
- •Production Application Chores
- •Moving On to Launch and Beyond
- •Appendices
- •Introduction to Ruby
- •Classes
- •Source Code
- •Resources
- •Index
- •Symbols
CREATING FOREIGN KEYS 324
Active Record to the rescue. Part of its ORM magic is that it converts the low-level foreign key relationships in the database into high-level interobject mappings. It handles the three basic cases.
•One row in table A is associated with zero or one rows in table B.
•One row in table A is associated with an arbitrary number of rows in table B.
•An arbitrary number of rows in table A are associated with an arbitrary number of rows in table B.
We have to give Active Record a little help when it comes to intertable relationships. This isn’t really Active Record’s fault—it isn’t possible to deduce from the schema what kind of intertable relationships the developer intended. However, the amount of help we have to supply is minimal.
18.1Creating Foreign Keys
As we discussed earlier, two tables are related when one table contains a foreign key reference to the primary key of another. In the following migrations, the table line_items contains foreign key references to the products and orders tables.
def self.up
create_table :products do |t| t.column :title, :string
# ...
end
create_table :orders do |t| t.column :name, :string
# ...
end
create_table :line_items do |t|
t.column :product_id, |
:integer |
t.column :order_id, |
:integer |
t.column :quantity, |
:integer, |
t.column :unit_price, |
:decimal, :precision => 8, :scale => 2 |
end |
|
end |
|
It’s worth noting that this migration doesn’t define any foreign key constraints. The intertable relationships are set up simply because the developer will populate the columns product_id and order_id with key values from the products and orders tables. You can also choose to establish these constraints in your migrations (and I personally recommend that you do), but the foreign key support in Rails doesn’t need them.
Report erratum
CREATING FOREIGN KEYS 325
Looking at this migration, we can see why it’s hard for Active Record to divine the relationships between tables automatically. The order_id and product_id foreign key references in the line_items table look identical. However, the product_id column is used to associate a line item with exactly one product. The order_id column is used to associate multiple line items with a single order. The line item is part of the order but references the product.
This example also shows the standard Active Record naming convention. The foreign key column should be named after the class of the target table, converted to lowercase, with _id appended. Note that between the pluralization and _id appending conventions, the assumed foreign key name will be consistently different from the name of the referenced table. If you have an Active Record model called Person, it will map to the database table people. A foreign key reference from some other table to the people table will have the column name person_id.
The other type of relationship is where some number of one item is related to some number of another item (such as products belonging to multiple categories and categories containing multiple products). The SQL convention for handling this uses a third table, called a join table. The join table contains a foreign key for each of the tables it’s linking, so each row in the join table represents a linkage between the two other tables. Here’s another migration.
def self.up
create_table :products do |t| t.column :title, :string
# ...
end
create_table :categories do |t| t.column :name, :string
# ...
end
create_table :categories_products, :id => false do |t| t.column :product_id, :integer
t.column :category_id, :integer end
# Indexes are important for performance if join tables grow big add_index :categories_products, [:product_id, :category_id] add_index :categories_products, :category_id
end
Rails assumes that a join table is named after the two tables it joins (with the names in alphabetical order). Rails will automatically find the join table categories_products linking categories and products. If you used some other name, you’ll need to add a declaration so Rails can find it.
Report erratum
SPECIFYING RELATIONSHIPS IN MODELS 326
Note that our join table does not need an id column for a primary key, because the combination of product and category id is unique. We stopped the migration from automatically adding the id column by specifying :id => false. We then created two indices on the join table. The first, composite index actually serves two purposes: it creates an index that can be searched on both foreign key columns, and with most databases it also creates an index that enables fast lookup by the product id. The second index then completes the picture, allowing fast lookup on category id.
18.2Specifying Relationships in Models
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.
One-to-One Relationships
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.
|
invoices |
|
|
orders |
|||
|
id |
|
|
|
|
|
|
|
|
|
|
|
id |
|
|
|
order_id |
|
|
|
|
name |
|
|
|
|
|
|
|
|
|
|
. . . |
|
|
|
|
|
|
|
|
|
|
|
. . . |
|
|
|
|
|
|
|
|
||
|
|
|
|
||||
class Invoice < ActiveRecord::Base |
class Order < ActiveRecord::Base |
||||||
belongs_to :order |
|
has_one :invoice |
|||||
# . . . |
|
|
# . . . |
|
|||
end |
end |
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.
One-to-Many Relationships
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.
Report erratum
SPECIFYING RELATIONSHIPS IN MODELS 327
|
line_items |
|
|
orders |
||||
|
|
|
|
|
|
|||
|
id |
|
|
|
|
id |
|
|
|
order_id |
|
|
|
|
name |
|
|
|
|
|
|
|
|
|
||
|
. . . |
|
|
|
|
|
|
|
|
|
|
|
|
. . . |
|
||
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
||
class LineItem < ActiveRecord::Base |
class Order < ActiveRecord::Base |
|||||||
|
has_many :line_items |
|||||||
belongs_to :order |
|
|||||||
# . . . |
|
|||||||
# . . . |
|
|
|
|||||
|
|
end |
||||||
end |
||||||||
|
|
|
|
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.
Many-to-Many Relationships
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.
|
categories |
|
|
|
categories_products |
products |
||||||
|
id |
|
|
|
category_id |
|
|
|
|
|
id |
|
|
|
|
|
|
|
|
|
|
|
|||
|
name |
|
|
|
product_id |
|
|
|
|
|
name |
|
|
|
|
|
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
. . . |
|
|
|
|
|
|
|
|
|
. . . |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Category< ActiveRecord::Base |
class Product< ActiveRecord::Base |
|||||||||||
|
has_and_belongs_to_many :products |
has_and_belongs_to_many :categories |
||||||||||
# . . . |
|
|
|
|
# . . . |
|
|
|
|
|
||
end |
|
|
|
|
end |
|
|
In Rails we express this by adding the has_and_belongs_to_many declaration to both models. From here on in, we’ll abbreviate this declaration to habtm.
Many-to-many associations are symmetrical—both of the joined tables declare their association with each other using habtm.
Within the database, many-to-many associations are implemented 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.
Report erratum