Participating in the Monitoring Process

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 sixteen 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.)

In the following figure we can see how Rails wraps the sixteen paired callbacks around the basic create, update, and destroy operations on model objects. Perhaps surprisingly, the before and after validation calls are not strictly nested.

images/ar_callbacks.png

The before_validation and after_validation calls also accept the on: :create or on: :update parameter, which will cause the callback to be called only on the selected operation.

In addition to these sixteen 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.

The preferred way to define a callback is to declare handlers. A handler can be either a method or a block. 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 < ApplicationRecord
  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!​(​/[-s]/​, ​''​)
 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 thows :abort, in which case the callback chain is broken early.

Alternately, you can define the callback instance methods using callback objects, inline methods (using a proc), or inline eval methods (using a string). See the online documentation for more details.[95]

Grouping Related Callbacks Together

If you have a group of related callbacks, it may be convenient to group them into a separate handler class. 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 models. 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!​(​/[-s]/​, ​''​)
 end
 end

Now, in our model classes, we can arrange for this shared callback to be invoked:

 class​ Order < ApplicationRecord
  before_validation CreditCardCallbacks.​new
 # ...
 end
 
 class​ Subscription < ApplicationRecord
  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 encrypt 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:

 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

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

We can now arrange for the Encrypter class to be invoked from inside our orders model:

 require ​"encrypter"
 class​ Order < ApplicationRecord
  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 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 ApplicationRecord class:

 class​ ApplicationRecord < ActiveRecord::Base
  self.​abstract_class​ = ​true
 
 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:

 class​ Order < ApplicationRecord
  encrypt(​:name​, ​:email​)
 end

A small driver program lets us experiment with this:

 o = Order.​new
 o.​name​ = ​"Dave Thomas"
 o.​address​ = ​"123 The Street"
 o.​email​ = ​"[email protected]"
 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​​ ​​encrypter.rb
 Dave Thomas
 Dave Thomas

In the database, however, the name and email address are obscured by our industrial-strength encryption:

 depot>​​ ​​sqlite3​​ ​​-line​​ ​​db/development.sqlite3​​ ​​"select * from orders"
  id = 1
 user_id =
  name = Dbwf Tipnbt
 address = 123 The Street
  email = [email protected]

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, in Participating in the Monitoring Process, 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.

When used in moderation, such an approach doesn’t lead to significant problems. If, however, you find yourself repeating code, consider using concerns[96] instead.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.82.167