- •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
ADVANCED ATTRIBUTES 380
Sometimes this convention breaks down. When it does, the observer class can explicitly list the model or models it wants to observe using the observe method.
Download e1/ar/observer.rb
class AuditObserver < ActiveRecord::Observer
observe Order, Payment, Refund
def after_save(model)
model.logger.info("#{model.class.name} #{model.id} created") end
end
AuditObserver.instance
In both these examples we’ve had to create an instance of the observer—merely defining the observer’s class does not enable that observer. For stand-alone Active Record applications, you’ll need to call the instance method at some convenient place during initialization. If you’re writing a Rails application, you’ll instead use the observer directive in your controller.
class StoreController < ApplicationController
observer :stock_control_observer
# ...
By convention, observer source files live in app/models.
In a way, observers bring to Rails much of the benefits of first-generation aspect-oriented programming in languages such as Java. They allow you to inject behavior into model classes without changing any of the code in those classes.
19.3Advanced Attributes
Back when we first introduced Active Record, we said that an Active Record object has attributes that correspond to the columns in the database table it wraps. We went on to say that this wasn’t strictly true. Here’s the rest of the story.
When Active Record first uses a particular model, it goes to the database and determines the column set of the corresponding table. From there it constructs a set of Column objects. These objects are accessible using the columns class method, and the Column object for a named column can be retrieved using the columns_hash method. The Column objects encode the database column’s name, type, and default value.
When Active Record reads information from the database, it constructs an SQL select statement. When executed, the select statement returns zero or
Report erratum
ADVANCED ATTRIBUTES 381
more rows of data. Active Record constructs a new model object for each of these rows, loading the row data into a hash, which it calls the attribute data. Each entry in the hash corresponds to an item in the original query. The key value used is the same as the name of the item in the result set.
Most of the time we’ll use a standard Active Record finder method to retrieve data from the database. These methods return all the columns for the selected rows. As a result, the attributes hash in each returned model object will contain an entry for each column, where the key is the column name and the value is the column data.
result = LineItem.find(:first) p result.attributes
{"order_id"=>13, "quantity"=>1, "product_id"=>27, "id"=>34, "unit_price"=>29.95}
Normally, we don’t access this data via the attributes hash. Instead, we use attribute methods.
result = LineItem.find(:first)
p |
result.quantity |
#=> |
1 |
p |
result.unit_price |
#=> |
29.95 |
But what happens if we run a query that returns values that don’t correspond to columns in the table? For example, we might want to run the following query as part of our application.
select quantity, quantity*unit_price from line_items;
If we manually run this query against our database, we might see something like the following.
mysql> select quantity, quantity*unit_price from line_items;
+----------+---------------------+ | quantity | quantity*unit_price |
+---------- |
|
+--------------------- |
|
+ |
| |
1 |
| |
29.95 |
| |
| |
2 |
| |
59.90 |
| |
| |
1 |
| |
44.95 |
| |
|
: |
|
: |
|
Notice that the column headings of the result set reflect the terms we gave to the select statement. These column headings are used by Active Record when populating the attributes hash. We can run the same query using Active Record’s find_by_sql method and look at the resulting attributes hash.
result = LineItem.find_by_sql("select quantity, quantity*unit_price " + "from line_items")
p result[0].attributes
Report erratum
ADVANCED ATTRIBUTES 382
The output shows that the column headings have been used as the keys in the attributes hash.
{"quantity*unit_price"=>"29.95", "quantity"=>1}
Note that the value for the calculated column is a string. Active Record knows the types of the columns in our table, but many databases do not return type information for calculated columns. In this case we’re using MySQL, which doesn’t provide type information, so Active Record leaves the value as a string. Had we been using Oracle, we’d have received a Float back, because the OCI interface can extract type information for all columns in a result set.
It isn’t particularly convenient to access the calculated attribute using the key quantity*price, so you’d normally rename the column in the result set using the as qualifier.
result = LineItem.find_by_sql("select quantity,
quantity*unit_price as total_price " +
" from line_items")
p result[0].attributes
This produces
{"total_price"=>"29.95", "quantity"=>1}
The attribute total_price is easier to work with.
result.each do |line_item|
puts "Line item #{line_item.id}: #{line_item.total_price}" end
Remember, though, that the values of these calculated columns will be stored in the attributes hash as strings. You’ll get an unexpected result if you try something like
TAX_RATE = 0.07
# ...
sales_tax = line_item.total_price * TAX_RATE
Perhaps surprisingly, the code in the previous example sets sales_tax to an empty string. The value of total_price is a string, and the * operator for strings duplicates their contents. Because TAX_RATE is less than 1, the contents are duplicated zero times, resulting in an empty string.
All is not lost! We can override the default Active Record attribute accessor methods and perform the required type conversion for our calculated field.
class LineItem < ActiveRecord::Base def total_price
Float(read_attribute("total_price")) end
end
Report erratum
TRANSACTIONS 383
Note that we accessed the internal value of our attribute using the method read_attribute, rather than by going to the attribute hash directly. The method read_attribute knows about database column types (including columns containing serialized Ruby data) and performs type conversion if required. This isn’t particularly useful in our current example but becomes more so when we look at ways of providing facade columns.
Facade Columns
Sometimes we use a schema where some columns are not in the most convenient format. For some reason (perhaps because we’re working with a legacy database or because other applications rely on the format), we cannot just change the schema. Instead our application just has to deal with it somehow. It would be nice if we could somehow put up a facade and pretend that the column data is the way we wanted it to be.
It turns out that we can do this by overriding the default attribute accessor methods provided by Active Record. For example, let’s imagine that our application uses a legacy product_data table—a table so old that product dimensions are stored in cubits.3 In our application we’d rather deal with inches,4 so let’s define some accessor methods that perform the necessary conversions.
class ProductData < ActiveRecord::Base CUBITS_TO_INCHES = 18
def length
read_attribute("length" ) * CUBITS_TO_INCHES end
def length=(inches)
write_attribute("length", Float(inches) / CUBITS_TO_INCHES) end
end
19.4Transactions
A database transaction groups a series of changes together in such a way that either all the changes are applied or none of the changes are applied. The classic example of the need for transactions (and one used in Active Record’s own documentation) is transferring money between two bank accounts. The basic logic is simple.
account1.deposit(100)
account2.withdraw(100)
3. A cubit is defined as the distance from your elbow to the tip of your longest finger. Because this is clearly subjective, the Egyptians standardized on the royal cubit, based on the king currently ruling. They even had a standards body, with a master cubit measured and marked on a granite
stone (http://www.ncsli.org/misc/cubit.cfm).
4. Inches, of course, are also a legacy unit of measure, but let’s not fight that battle here.
Report erratum
TRANSACTIONS 384
However, we have to be careful. What happens if the deposit succeeds but for some reason the withdrawal fails (perhaps the customer is overdrawn)? We’ll have added $100 to the balance in account1 without a corresponding deduction from account2. In effect we’ll have created $100 out of thin air.
Transactions to the rescue. A transaction is something like the Three Musketeers with their motto “All for one and one for all.” Within the scope of a transaction, either every SQL statement succeeds or they all have no effect. Putting that another way, if any statement fails, the entire transaction has no effect on the database.5
In Active Record we use the transaction method to execute a block in the context of a particular database transaction. At the end of the block, the transaction is committed, updating the database, unless an exception is raised within the block, in which case all changes are rolled back and the database is left untouched. Because transactions exist in the context of a database connection, we have to invoke them with an Active Record class as a receiver. Thus we could write
Account.transaction do account1.deposit(100) account2.withdraw(100)
end
Let’s experiment with transactions. We’ll start by creating a new database table. (Make sure your database supports transactions, or this code won’t work for you.)
Download e1/ar/transactions.rb
create_table :accounts, :force => true do |t| t.column :number, :string
t.column :balance, :decimal, :precision => 10, :scale => 2, :default => 0 end
Next, we’ll define a simple bank account class. This class defines instance methods to deposit money to and withdraw money from the account. It also provides some basic validation—for this particular type of account, the balance can never be negative.
Download e1/ar/transactions.rb
class Account < ActiveRecord::Base
def withdraw(amount) adjust_balance_and_save(-amount)
end
5. Transactions are actually more subtle than that. They exhibit the so-called ACID properties: they’re Atomic, they ensure Consistency, they work in Isolation, and their effects are Durable (they are made permanent when the transaction is committed). It’s worth finding a good database book and reading up on transactions if you plan to take a database application live.
Report erratum
TRANSACTIONS 385
def deposit(amount) adjust_balance_and_save(amount)
end
private
def adjust_balance_and_save(amount) self.balance += amount
save! end
def validate # validation is called by Active Record errors.add(:balance, "is negative") if balance < 0
end end
Let’s look at the helper method, adjust_balance_and_save. The first line simply updates the balance field. The method then calls save! to save the model data. (Remember that save! raises an exception if the object cannot be saved—we use the exception to signal to the transaction that something has gone wrong.)
So now let’s write the code to transfer money between two accounts. It’s pretty straightforward.
Download e1/ar/transactions.rb
peter = Account.create(:balance => 100, :number => "12345") paul = Account.create(:balance => 200, :number => "54321")
Download e1/ar/transactions.rb
Account.transaction do paul.deposit(10) peter.withdraw(10)
end
We check the database, and, sure enough, the money got transferred.
mysql> |
select |
* from accounts; |
||||
+---- |
|
+-------- |
|
+--------- |
|
+ |
| id |
| number |
| balance | |
||||
+---- |
|
+-------- |
|
+--------- |
|
+ |
| |
5 |
| 12345 |
| |
90.00 |
| |
|
| |
6 |
| 54321 |
| |
210.00 |
| |
|
+---- |
|
+-------- |
|
+--------- |
|
+ |
Now let’s get radical. If we start again but this time try to transfer $350, we’ll run Peter into the red, which isn’t allowed by the validation rule. Let’s try it.
Download e1/ar/transactions.rb
peter = Account.create(:balance => 100, :number => "12345") paul = Account.create(:balance => 200, :number => "54321")
Report erratum
TRANSACTIONS 386
Download e1/ar/transactions.rb
Account.transaction do paul.deposit(350) peter.withdraw(350)
end
When we run this, we get an exception reported on the console.
.../validations.rb:736:in ‘save!': Validation failed: Balance is negative from transactions.rb:46:in ‘adjust_balance_and_save'
: : :
from transactions.rb:80
Looking in the database, we can see that the data remains unchanged.
mysql> |
select |
* from accounts; |
||||
+---- |
|
+-------- |
|
+--------- |
|
+ |
| id |
| number |
| balance | |
||||
+---- |
|
+-------- |
|
+--------- |
|
+ |
| |
7 |
| 12345 |
| |
100.00 |
| |
|
| |
8 |
| 54321 |
| |
200.00 |
| |
|
+---- |
|
+-------- |
|
+--------- |
|
+ |
However, there’s a trap waiting for you here. The transaction protected the database from becoming inconsistent, but what about our model objects? To see what happened to them, we have to arrange to intercept the exception to allow the program to continue running.
Download e1/ar/transactions.rb
peter = Account.create(:balance => 100, :number => "12345") paul = Account.create(:balance => 200, :number => "54321")
Download e1/ar/transactions.rb
begin
Account.transaction do paul.deposit(350) peter.withdraw(350)
end rescue
puts "Transfer aborted" end
puts "Paul has #{paul.balance}" puts "Peter has #{peter.balance}"
What we see is a little surprising.
Transfer aborted
Paul has 550.0
Peter has -250.0
Although the database was left unscathed, our model objects were updated anyway. This is because Active Record wasn’t keeping track of the before and after states of the various objects—in fact it couldn’t, because it had no easy
Report erratum
TRANSACTIONS 387
way of knowing just which models were involved in the transactions. We can rectify this by listing them explicitly as parameters to the transaction method.
Download e1/ar/transactions.rb
peter = Account.create(:balance => 100, :number => "12345") paul = Account.create(:balance => 200, :number => "54321")
Download e1/ar/transactions.rb
begin
Account.transaction(peter, paul) do paul.deposit(350) peter.withdraw(350)
end rescue
puts "Transfer aborted" end
puts "Paul has #{paul.balance}" puts "Peter has #{peter.balance}"
This time we see the models are unchanged at the end.
Transfer aborted
Paul has 200.0
Peter has 100.0
We can tidy this code a little by moving the transfer functionality into the Account class. Because a transfer involves two separate accounts, and isn’t driven by either of them, we’ll make it a class method that takes two account objects as parameters. Notice how we can simply call the transaction method inside the class method.
Download e1/ar/transactions.rb
class Account < ActiveRecord::Base def self.transfer(from, to, amount)
transaction(from, to) do from.withdraw(amount) to.deposit(amount)
end end
end
With this method defined, our transfers are a lot tidier.
Download e1/ar/transactions.rb
peter = Account.create(:balance => 100, :number => "12345") paul = Account.create(:balance => 200, :number => "54321")
Download e1/ar/transactions.rb
Account.transfer(peter, paul, 350) rescue puts "Transfer aborted"
puts "Paul has #{paul.balance}" puts "Peter has #{peter.balance}"
Report erratum
TRANSACTIONS 388
Transfer aborted
Paul has 200.0
Peter has 100.0
There’s a downside to having the transaction code recover the state of objects automatically—you can’t get to any error information added during validation. Invalid objects won’t be saved, and the transaction will roll everything back, but there’s no easy way of knowing what went wrong.
Built-in Transactions
When we discussed parent and child tables, we said that Active Record takes care of saving all the dependent child rows when you save a parent row. This takes multiple SQL statement executions (one for the parent and one each for any changed or new children). Clearly this change should be atomic, but until now we haven’t been using transactions when saving these interrelated objects. Have we been negligent?
Fortunately not. Active Record is smart enough to wrap all of the updates and inserts related to a particular save (and also the deletes related to a destroy) in a transaction; either they all succeed or no data is written permanently to the database. You need explicit transactions only when you manage multiple SQL statements yourself.
Multidatabase Transactions
How do you go about synchronizing transactions across a number of different databases in Rails?
The current answer is that you can’t. Rails doesn’t support distributed twophase commits (which is the jargon term for the protocol that lets databases synchronize with each other).
However, you can (almost) simulate the effect by nesting transactions. Remember that transactions are associated with database connections, and connections are associated with models. So, if the accounts table is in one database and users is in another, you could simulate a transaction spanning the two using something such as
User.transaction(user) do Account.transaction(account) do
account.calculate_fees user.date_fees_last_calculated = Time.now user.save
account.save end
end
This is only an approximation to a solution. It is possible that the commit in the users database might fail (perhaps the disk is full), but by then the commit
Report erratum
TRANSACTIONS 389
Application Database
id |
123 |
|
|
123 |
|
123 |
|||
name |
Dave |
|
Fred |
Dave |
|||||
pay_type |
check |
|
check |
po |
|||||
etc... |
... |
|
|
... |
|
... |
|||
|
|
|
|
|
|
|
|
||
process 1 |
|
|
o = Order.find(123) |
|
|
o.name= 'Fred' |
|
|
|
|
|
|
|
|
|
o.save |
|
|
|
|
|
|
|
|
|
|
|
||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
process 2 |
|
|
o = Order.find(123) |
|
|
o.pay_type = 'po' |
|
||
|
|
|
|
|
|
|
o.save |
|
|
|
|
|
|
|
|
|
|
|
|
Figure 19.2: Race Condition: Second Update Overwrites First
in the accounts database has completed and the table has been updated. This would leave the overall transaction in an inconsistent state. It is possible (if not pleasant) to code around these issues for each individual set of circumstances, but for now, you probably shouldn’t be relying on Active Record if you are writing applications that update multiple databases concurrently.
Optimistic Locking
In an application where multiple processes access the same database, it’s possible for the data held by one process to become stale if another process updates the underlying database row.
For example, two processes may fetch the row corresponding to a particular account. Over the space of several seconds, both go to update that balance. Each loads an Active Record model object with the initial row contents. At different times they each use their local copy of the model to update the underlying row. The result is a race condition in which the last person to update the row wins and the first person’s change is lost. This is shown in Figure 19.2.
One solution to the problem is to lock the tables or rows being updated. By preventing others from accessing or updating them, locking overcomes concurrency issues, but it’s a fairly brute-force solution. It assumes that something will go wrong and locks just in case. For this reason, the approach is often called pessimistic locking. Pessimistic locking is unworkable for web applications if you need to ensure consistency across multiple user requests, because
Report erratum
TRANSACTIONS 390
it is very hard to manage the locks in such a way that the database doesn’t grind to a halt.
Optimistic locking doesn’t take explicit locks. Instead, just before it writes updated data back to a row, it checks to make sure that no one else has already changed that row. In the Rails implementation, each row contains a version number. Whenever a row is updated, the version number is incremented. When you come to do an update from within your application, Active Record checks the version number of the row in the table against the version number of the model doing the updating. If the two don’t match, it abandons the update and throws an exception.
Optimistic locking is enabled by default on any table that contains an integer column called lock_version. You should arrange for this column to be initialized to zero for new rows, but otherwise you should leave it alone—Active Record manages the details for you.
Let’s see optimistic locking in action. We’ll create a table called counters containing a simple count field along with the lock_version column. (Note the :default setting on the lock_version column.)
Download e1/ar/optimistic.rb
create_table :counters, :force => true do |t|
t.column :count, |
:integer |
t.column :lock_version, :integer, :default => 0 end
Then we’ll create a row in the table, read that row into two separate model objects, and try to update it from each.
Download e1/ar/optimistic.rb
class Counter < ActiveRecord::Base end
Counter.delete_all
Counter.create(:count => 0)
count1 = Counter.find(:first) count2 = Counter.find(:first)
count1.count += 3 count1.save
count2.count += 4 count2.save
When we run this, we see an exception. Rails aborted the update of count2 because the values it held were stale.
Report erratum
TRANSACTIONS 391
/opt/local/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/locking.rb:47: in ‘update_without_callbacks': Attempted to update a stale object
(ActiveRecord::StaleObjectError)
If you use optimistic locking, you’ll need to catch these exceptions in your application.
You can disable optimistic locking with
ActiveRecord::Base.lock_optimistically = false
You can change the name of the column used to keep track of the version number on a per-model basis.
class Change < ActiveRecord::Base set_locking_column("generation_number")
# ...
end
Report erratum