- •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
MANAGING TABLES 269
but specify the name of an existing column. Let’s say that the order type column is currently an integer, but we need to change it to be a string. We want to keep the existing data, so an order type of 123 will become the string "123". Later, we’ll use noninteger values such as "new" and "existing".
Changing from an integer column to a string is easy.
def self.up
change_column :orders, :order_type, :string, :null => false end
However, the opposite transformation is problematic. We might be tempted to write the obvious down migration.
def self.down
change_column :orders, :order_type, :integer end
But if our application has taken to storing data like "new" in this column, the down method will lose it—"new" can’t be converted to an integer. If that’s acceptable, then the migration is acceptable as it stands. If, however, we want to create a one-way migration—one that cannot be reversed—you’ll want to stop the down migration from being applied. In this case, Rails provides a special exception that you can throw.
class ChangeOrderTypeToString < ActiveRecord::Migration def self.up
change_column :orders, :order_type, :string, :null => false end
def self.down
raise ActiveRecord::IrreversibleMigration
end end
16.3Managing Tables
So far we’ve been using migrations to manipulate the columns in existing tables. Now let’s look at creating and dropping tables.
class |
CreateOrderHistories |
< ActiveRecord::Migration |
def |
self.up |
|
create_table :order_histories do |t| |
||
|
t.column :order_id, |
:integer, :null => false |
|
t.column :created_at, :timestamp |
|
|
t.column :notes, |
:text |
end |
|
|
end |
|
|
def |
self.down |
|
drop_table :order_histories end
end
Report erratum
MANAGING TABLES 270
create_table takes the name of a table (remember, table names are plural) and a block. (It also takes some optional parameters that we’ll look at in a minute.) The block is passed a table definition object, which we use to define the columns in the table by calling its column method.
The calls to column should look familiar—they’re identical to the add_column method we used previously except they don’t take the name of the table as the first parameter.
Note that we don’t define the id column for our new table. Unless we say otherwise, Rails migrations automatically add a primary key called id to all tables it creates. For a deeper discussion of this, see Section 16.3, Primary Keys, on page 273.
Options for Creating Tables
You can pass a hash of options as a second parameter to create_table.
If you specify :force => true, the migration will drop an existing table of the same name before creating the new one. This is a useful option if you want to create a migration that forces a database into a known state, but there’s clearly a potential for data loss.
The :temporary => true option creates a temporary table—one that goes away when the application disconnects from the database. This is clearly pointless in the context of a migration, but as we’ll see later, it does have its uses elsewhere.
The :options => "xxxx" parameter lets you specify options to your underlying database. These are added to the end of the CREATE TABLE statement, right after the closing parenthesis. For example, some versions of MySQL allow you to specify the initial value of the autoincrementing id column. We can pass this in through a migration as follows.
create_table :tickets, :options => "auto_increment = 10000" do |t| t.column :created_at, :timestamp
t.column :description, :text end
Behind the scenes, migrations will generate the following DDL from this table description.
create table tickets (
‘id‘ int(11) default null auto_increment primary key, ‘created_at‘ datetime,
‘description‘ text
) auto_increment = 10000;
Be careful when using the :options parameter with MySQL. The Rails MySQL database adapter sets a default option of ENGINE=InnoDB. This overrides any
Report erratum
MANAGING TABLES 271
local defaults you may have and forces migrations to use the InnoDB storage engine for new tables. However, if you override :options, you’ll lose this setting; new tables will be created using whatever database engine is configured as the default for your site. You may want to add an explicit ENGINE=InnoDB to the options string to force the standard behavior in this case.2
Renaming Tables
If refactoring leads us to rename variables and columns, then it’s probably not a surprise that we sometimes find ourselves renaming tables, too. Migrations support the rename_table method.
class RenameOrderHistories < ActiveRecord::Migration def self.up
rename_table :order_histories, :order_notes end
def self.down
rename_table :order_notes, :order_histories end
end
Note how the down method undoes the change by renaming the table back.
Problems with rename_table
There’s a subtle problem when you rename tables in migrations.
For example, let’s assume that in migration 4 you create the order_histories table and populate it with some data.
def self.up
create_table :order_histories do |t|
t.column |
:order_id, |
:integer, :null => false |
t.column |
:created_at, |
:timestamp |
t.column |
:notes, |
:text |
end |
|
|
order = Order.find :first
OrderHistory.create(:order => order, :notes => "test") end
Later, in migration 7, you rename the table order_histories to order_notes. At this point you’ll also have renamed the model OrderHistory to OrderNote.
Now you decide to drop your development database and reapply all migrations. When you do so, the migrations throw an exception in migration 4: your application no longer contains a class called OrderHistory, so the migration fails.
2. You probably want to keep using InnoDB if you’re using MySQL, because this engine gives you transaction support. You might need transaction support in your application, and you’ll definitely need it in your tests if you’re using the default of transactional test fixtures.
Report erratum
MANAGING TABLES 272
One solution, proposed by Tim Lucas, is to create local, dummy versions of the model classes needed by a migration within the migration itself. For example, the following version of the fourth migration will work even if the application no longer has an OrderHistory class.
class CreateOrderHistories < ActiveRecord::Migration
class Order < ActiveRecord::Base; end
class OrderHistory < ActiveRecord::Base; end
def self.up
create_table :order_histories do |t|
t.column :order_id, |
:integer, :null => false |
t.column :created_at, |
:timestamp |
t.column :notes, |
:text |
end |
|
order = Order.find :first
OrderHistory.create(:order = order, :notes => "test") end
def self.down
drop_table :order_histories end
end
This works as long as your model classes do not contain any additional functionality that would have been used in the migration—all you’re creating here is a bare-bones version.
If renaming tables gets to be a problem for you, I recommend consolidating your migrations as described in Section 16.8, Managing Migrations, on page 281.
Defining Indices
Migrations can (and probably should) define indices for tables. For example, you might notice that once your application has a large number of orders in the database, searching based on the customer’s name takes longer than you’d like. Time to add an index using the appropriately named add_index method.
class AddCustomerNameIndexToOrders < ActiveRecord::Migration def self.up
add_index :orders, :name end
def self.down
remove_index :orders, :name end
end
Report erratum
MANAGING TABLES 273
If you give add_index the optional parameter :unique => true, a unique index will be created, forcing values in the indexed column to be unique.
By default the index will be given the name table_column_index. You can override this using the :name => "somename" option. If you use the :name option when adding an index, you’ll also need to specify it when removing the index.
You can create a composite index—an index on multiple columns—by passing an array of column names to add_index. In this case only the first column name will be used when naming the index.
Primary Keys
Rails assumes that every table has a numeric primary key (normally called id). Rails ensures the value of this column is unique for each new row added to a table.
Let me rephrase that.
Rails really doesn’t work too well unless each table has a numeric primary key. It is less fussy about the name of the column.
So, for your average Rails application, my strong advice is to go with the flow and let Rails have its id column.
If you decide to be adventurous, you can start by using a different name for the primary key column (but keeping it as an incrementing integer). Do this by specifying a :primary_key option on the create_table call.
create_table :tickets, :primary_key => :number do |t| t.column :created_at, :timestamp
t.column :description, :text end
This adds the number column to the table and sets it up as the primary key.
mysql> describe tickets; |
|
|
|
|
|
|
+-------------- |
+---------- |
+------ |
+----- |
+--------- |
+---------------- |
+ |
| Field |
| Type |
| Null | Key | Default |
| Extra |
| |
||
+-------------- |
+---------- |
+------ |
+----- |
+--------- |
+---------------- |
+ |
| number |
| int(11) |
| NO |
| PRI | NULL |
| auto_increment |
| |
|
| created_at |
| datetime |
| YES |
| |
| NULL |
| |
| |
| description |
| text |
| YES |
| |
| NULL |
| |
| |
+-------------- |
+---------- |
+------ |
+----- |
+--------- |
+---------------- |
+ |
3 rows in set (0.34 sec)
The next step in the adventure might be to create a primary key that isn’t an integer. Here’s a clue that the Rails developers don’t think this is a good idea: migrations don’t let you do this (at least not directly).
Report erratum