- •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
CALLBACKS 373
:confirmation => "doesn't match confirmation"
:empty |
=> "can't be empty" |
:exclusion |
=> "is reserved" |
:inclusion |
=> "is not included in the list" |
:invalid |
=> "is invalid" |
:not_a_number => "is not a number" |
|
:taken |
=> "has already been taken" |
:too_long |
=> "is too long (maximum is %d characters)" |
:too_short |
=> "is too short (minimum is %d characters)" |
:wrong_length |
=> "is the wrong length (should be %d characters)" |
To change the message returned if the uniqueness validation fails, you could code something like
ActiveRecord::Errors.default_error_messages[:taken] = "is in use"
You’ll probably want to put this in the environment.rb file in your application’s config directory.
19.2Callbacks
Active Record controls the life cycle of model objects—it creates them, monitors them as they are modified, saves and updates them, and watches sadly as they are destroyed. Using callbacks, Active Record lets our code participate in this monitoring process. We can write code that gets invoked at any significant event in the life of an object. With these callbacks we can perform complex validation, map column values as they pass in and out of the database, and even prevent certain operations from completing.
Active Record defines 20 callbacks. Eighteen of these form before/after pairs and bracket some operation on an Active Record object. For example, the before_destroy callback will be invoked just before the destroy method is called, and after_destroy will be invoked after. The two exceptions are after_find and after_initialize, which have no corresponding before_xxx callback. (These two callbacks are different in other ways, too, as we’ll see later.)
Figure 19.1, on the following page, shows how the 18 paired callbacks are wrapped around the basic create, update, and destroy operations on model objects. Perhaps surprisingly, the before and after validation calls are not strictly nested.
In addition to these 18 calls, the after_find callback is invoked after any find operation, and after_initialize is invoked after an Active Record model object is created.
To have your code execute during a callback, you need to write a handler and associate it with the appropriate callback.
There are two basic ways of implementing callbacks.
Report erratum
CALLBACKS 374
model.save() |
|
model.destroy() |
|
new record |
existing record |
|
|
before_validation |
before_validation |
|
|
|
|
||
before_validation_on_create |
before_validation_on_update |
|
|
after_validation |
after_validation |
|
|
after_validation_on_create |
after_validation_on_update |
|
|
before_save |
before_save |
|
|
before_create |
before_update |
before_destroy |
|
|
|
|
|
insert operation |
update operation |
delete operation |
|
|
|
|
|
after_create |
after_update |
after_destroy |
|
after_save |
after_save |
|
|
Figure 19.1: Sequence of Active Record Callbacks
First, you can define the callback instance method directly. If you want to handle the before save event, for example, you could write
class Order < ActiveRecord::Base
# ..
def before_save
self.payment_due ||= Time.now + 30.days end
end
The second basic way to define a callback is to declare handlers. A handler can be either a method or a block.1 You associate a handler with a particular event using class methods named after the event. To associate a method, declare it as private or protected, and specify its name as a symbol to the handler declaration. To specify a block, simply add it after the declaration. This block receives the model object as a parameter.
class Order < ActiveRecord::Base
before_validation :normalize_credit_card_number
after_create do |order|
logger.info "Order #{order.id} created" end
protected
1. A handler can also be a string containing code to be evaled, but this is deprecated.
Report erratum
CALLBACKS 375
def normalize_credit_card_number self.cc_number.gsub!(/-\w/, '')
end end
You can specify multiple handlers for the same callback. They will generally be invoked in the order they are specified unless a handler returns false (and it must be the actual value false), in which case the callback chain is broken early.
Because of a performance optimization, the only way to define callbacks for the after_find and after_initialize events is to define them as methods. If you try declaring them as handlers using the second technique, they’ll be silently ignored. (Sometimes folks ask why this was done. Rails has to use reflection to determine whether there are callbacks to be invoked. When doing real database operations, the cost of doing this is normally not significant compared to the database overhead. However, a single database select statement could return hundreds of rows, and both callbacks would have to be invoked for each. This slows the query down significantly. The Rails team decided that performance trumps consistency in this case.)
Time-Stamping Records
One potential use of the before_create and before_update callbacks is timestamping rows.
class Order < ActiveRecord::Base def before_create
self.order_created ||= Time.now end
def before_update self.order_modified = Time.now
end end
However, Active Record can save you the trouble of doing this. If your database table has a column named created_at or created_on, it will automatically be set to the time stamp of the row’s creation time. Similarly, a column named updated_at or updated_on will be set to the time stamp of the latest modification. These time stamps will by default be in local time; to make them UTC (also known as GMT), include the following line in your code (either inline for stand-alone Active Record applications or in an environment file for a full Rails application).
ActiveRecord::Base.default_timezone = :utc
To disable this behavior altogether, use
ActiveRecord::Base.record_timestamps = false
Report erratum
CALLBACKS 376
Callback Objects
As a variant to specifying callback handlers directly in the model class, you can create separate handler classes that encapsulate all the callback methods. These handlers can be shared between multiple models. A handler class is simply a class that defines callback methods (before_save, after_create, and so on). Create the source files for these handler classes in app/models.
In the model object that uses the handler, you create an instance of this handler class and pass that instance to the various callback declarations. A couple of examples will make this clearer.
If our application uses credit cards in multiple places, we might want to share our normalize_credit_card_number method across multiple methods. To do that, we’d extract the method into its own class and name it after the event we want it to handle. This method will receive a single parameter, the model object that generated the callback.
class CreditCardCallbacks
# Normalize the credit card number def before_validation(model)
model.cc_number.gsub!(/-\w/, '') end
end
Now, in our model classes, we can arrange for this shared callback to be invoked.
class Order < ActiveRecord::Base before_validation CreditCardCallbacks.new
# ...
end
class Subscription < ActiveRecord::Base before_validation CreditCardCallbacks.new
# ...
end
In this example, the handler class assumes that the credit card number is held in a model attribute named cc_number; both Order and Subscription would have an attribute with that name. But we can generalize the idea, making the handler class less dependent on the implementation details of the classes that use it.
For example, we could create a generalized encryption and decryption handler. This could be used to encrypt named fields before they are stored in the database and to decrypt them when the row is read back. You could include it as a callback handler in any model that needed the facility.
Report erratum
CALLBACKS 377
The handler needs to encrypt2 a given set of attributes in a model just before that model’s data is written to the database. Because our application needs to deal with the plain-text versions of these attributes, it arranges to decrypt them again after the save is complete. It also needs to decrypt the data when a row is read from the database into a model object. These requirements mean we have to handle the before_save, after_save, and after_find events. Because we need to decrypt the database row both after saving and when we find a new row, we can save code by aliasing the after_find method to after_save—the same method will have two names.
Download e1/ar/encrypt.rb
class Encrypter
#We're passed a list of attributes that should
#be stored encrypted in the database
def initialize(attrs_to_manage) @attrs_to_manage = attrs_to_manage
end
#Before saving or updating, encrypt the fields using the NSA and
#DHS approved Shift Cipher
def before_save(model) @attrs_to_manage.each do |field|
model[field].tr!("a-z", "b-za") end
end
#After saving, decrypt them back def after_save(model)
@attrs_to_manage.each do |field| model[field].tr!("b-za" , "a-z")
end end
#Do the same after finding an existing record alias_method :after_find, :after_save
end
We can now arrange for the Encrypter class to be invoked from inside our orders model.
require "encrypter"
class Order < ActiveRecord::Base
encrypter = Encrypter.new(:name, :email)
before_save encrypter after_save encrypter after_find encrypter
2. Our example here uses trivial encryption—you might want to beef it up before using this class for real.
Report erratum
CALLBACKS 378
protected
def after_find end
end
We create a new Encrypter object and hook it up to the events before_save, after_save, and after_find. This way, just before an order is saved, the method before_save in the encrypter will be invoked, and so on.
So, why do we define an empty after_find method? Remember that we said that for performance reasons after_find and after_initialize are treated specially. One of the consequences of this special treatment is that Active Record won’t know to call an after_find handler unless it sees an actual after_find method in the model class. We have to define an empty placeholder to get after_find processing to take place.
This is all very well, but every model class that wants to use our encryption handler would need to include some eight lines of code, just as we did with our Order class. We can do better than that. We’ll define a helper method that does all the work and make that helper available to all Active Record models. To do that, we’ll add it to the ActiveRecord::Base class.
Download e1/ar/encrypt.rb
class ActiveRecord::Base
def self.encrypt(*attr_names)
encrypter = Encrypter.new(attr_names)
before_save encrypter after_save encrypter after_find encrypter
define_method(:after_find) { } end
end
Given this, we can now add encryption to any model class’s attributes using a single call.
Download e1/ar/encrypt.rb
class Order < ActiveRecord::Base encrypt(:name, :email)
end
A simple driver program lets us experiment with this.
Download e1/ar/encrypt.rb
o = Order.new
o.name = "Dave Thomas" o.address = "123 The Street" o.email = "dave@pragprog.com"
Report erratum
CALLBACKS 379
o.save
puts o.name
o = Order.find(o.id) puts o.name
On the console, we see our customer’s name (in plain text) in the model object.
ar> ruby encrypt.rb
Dave Thomas
Dave Thomas
In the database, however, the name and e-mail address are obscured by our industrial-strength encryption.
ar> |
mysql -urailsuser -prailspw |
railsdb |
|
|
|
||||
mysql> |
select * from |
orders; |
|
|
|
|
|||
+---- |
|
+------------- |
|
+------------------- |
|
+------------- |
---+---------- |
+-------------- |
+ |
| id |
| name |
| |
| address |
| pay_type | when_shipped | |
|||||
+---- |
|
+------------- |
|
+------------------- |
|
+---------------- |
+---------- |
+-------------- |
+ |
| |
1 |
| Dbwf Tipnbt |
| |
ebwf@qsbhqsph.dpn | 123 The Street | |
| |
NULL | |
|||
+---- |
|
+------------- |
|
+------------------- |
|
+---------------- |
+---------- |
+-------------- |
+ |
1 |
row in set (0.00 |
sec) |
|
|
|
|
Observers
Callbacks are a fine technique, but they can sometimes result in a model class taking on responsibilities that aren’t really related to the nature of the model. For example, on page 374 we created a callback that generated a log message when an order was created. That functionality isn’t really part of the basic Order class—we put it there because that’s where the callback executed.
Active Record observers overcome that limitation. An observer transparently links itself into a model class, registering itself for callbacks as if it were part of the model but without requiring any changes in the model itself. Here’s our previous logging example written using an observer.
Download e1/ar/observer.rb
class OrderObserver < ActiveRecord::Observer def after_save(an_order)
an_order.logger.info("Order #{an_order.id} created") end
end
OrderObserver.instance
When ActiveRecord::Observer is subclassed, it looks at the name of the new class, strips the word Observer from the end, and assumes that what is left is the name of the model class to be observed. In our example, we called our observer class OrderObserver, so it automatically hooked itself into the model
Order.
Report erratum