Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 1st Edition (2005).pdf
Скачиваний:
28
Добавлен:
17.08.2013
Размер:
7.99 Mб
Скачать

CALLBACKS 264

validates_uniqueness_of

Validates that attributes are unique.

validates_uniqueness_of attr... [ options... ]

For each attribute, validates that no other row in the database currently has the same value in that given column. When the model object comes from an existing database row, that row is ignored when performing the check. The optional :scope parameter can be used to filter the rows tested to those having the same value in the :scope column as the current record.

This code ensures that user names are unique across the database.

class User < ActiveRecord::Base validates_uniqueness_of :name

end

This code ensures that user names are unique within a group.

class User < ActiveRecord::Base validates_uniqueness_of :name, :scope => "group_id"

end

Options:

 

 

:message

text

Default is “has already been taken.”

:on

 

:save, :create, or :update

:scope

attr

Limits the check to rows having the same value in the column as the row

 

 

being checked.

15.5 Callbacks

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.

We’ve already seen this facility in action. When we added user maintenance code to our Depot application, we wanted to ensure that our administrators couldn’t delete the magic user Dave from the database, so we added the following callback to the User class.

class User < ActiveRecord::Base before_destroy :dont_destroy_dave

def dont_destroy_dave

raise "Can't destroy dave" if name == 'dave' end

end

Prepared exclusively for Rida Al Barazi

Report erratum

CALLBACKS 265

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 15.4: Sequence of Active Record Callbacks

The before_destroy call registers the dont_destroy_dave( ) method as a callback to be invoked before user objects are destroyed. If an attempt is made to destroy the Dave user (for shame), this method raises an exception and the row will not be deleted.

Active Record defines 16 callbacks. Fourteen 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 15.4 shows how the 14 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 14 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.

Prepared exclusively for Rida Al Barazi

Report erratum

CALLBACKS 266

Joe Asks. . .

Why Are after_find and after_initialize Special?

Rails has to use reflection to determine if 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 things down significantly. The Rails team decided that performance trumps consistency in this case.

There are two basic ways of implementing 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.3 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

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

3A handler can also be a string containing code to be eval( )ed, but this is deprecated.

Prepared exclusively for Rida Al Barazi

Report erratum

CALLBACKS 267

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.

Timestamping 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 timestamp of the row’s creation time. Similarly, a column named updated_at or updated_on will be set to the timestamp of the latest modification. These timestamps 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 standalone 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

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 a lot clearer.

Prepared exclusively for Rida Al Barazi

Report erratum

CALLBACKS 268

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.

The handler needs to encrypt4 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.

4Our example here uses trivial encryption—you might want to beef it up before using this class for real.

Prepared exclusively for Rida Al Barazi

Report erratum

CALLBACKS 269

File 9

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

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 make use of 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

Prepared exclusively for Rida Al Barazi

Report erratum

CALLBACKS 270

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.

File 9

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.

File 9

class Order < ActiveRecord::Base

 

 

 

 

 

 

encrypt(:name, :email)

 

 

 

 

 

end

 

 

 

 

 

 

A simple driver program lets us experiment with this.

 

 

 

File 9

o = Order.new

 

 

 

 

 

 

o.name = "Dave Thomas"

 

 

 

 

 

o.address = "123 The Street"

 

 

 

 

 

o.email = "dave@pragprog.com"

 

 

 

 

 

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

| email

| 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 266 we created a callback that generated

Prepared exclusively for Rida Al Barazi

Report erratum

 

CALLBACKS

271

 

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 links itself

 

 

transparently 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.

 

File 12

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.

 

 

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.

 

File 12

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 ApplicationController, as we’ll see on page 278.

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.

Prepared exclusively for Rida Al Barazi

Report erratum