Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 55 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
55
Dung lượng
394,38 KB
Nội dung
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. How- ever, 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 con- sistency 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 han- dler 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 sym- bol 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 gen- erally be invoked in the order they are specified unless a handler returns 3 A handler can also be a string containing code to be eval( )ed, but this is deprecated. 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 Onepotentialuseofthebefore_create and before_update callbacks is time- stamping 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 auto- matically be set to the timestamp of the row’s creation time. Similarly, acolumnnamed 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 han- dler 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. 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;bothOrder 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 han- dler. 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 4 a given set of attributes in a model just before that model’s data is written to the database. Because our appli- cation 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. 4 Our example here uses trivial encryption—you might want to beef it up before using this class for real. 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,andafter_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 place- holder 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 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 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 Applica- tionController , 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. Report erratum ADVANCED ATTRIBUTES 272 15.6 Advanced Attributes Back when we first introduced Active Record, we said that an Active Record object has attributes that correspond to the columns in the underlying database table. 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. TheColumn 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 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 corre- spond 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; Report erratum ADVANCED ATTRIBUTES 273 If we manually run this query against our database, we might see some- thing 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 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, as 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 Report erratum ADVANCED ATTRIBUTES 274 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 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 read_attribute( ) method 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. 5 In our application we’d rather deal with 5 A cubit is defined as the distance from your elbow to the tip of your longest finger. As 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). Report erratum MISCELLANY 275 inches, 6 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 15.7 Miscellany This section contains various Active Record–related topics that just didn’t seem to fit anywhere else. Object Identity Model objects redefine the Ruby id() and hash( ) methods to reference the model’s primary key. This means that model objects with valid ids may be used as hash keys. It also means that unsaved model objects cannot reliably be used as hash keys (as they won’t yet have a valid id). Two model objects are considered equal (using ==) if they are instances of the same class and have the same primary key. This means that unsaved model objects may compare as equal even if they have different attribute data. If you find yourself comparing unsaved model objects (which is not a particularly frequent operation), you might need to override the == method. Using the Raw Connection You can execute SQL statements using the underlying Active Record con- nection adapter. This is useful for those (rare) circumstances when you need to interact with the database outside the context of an Active Record model class. At the lowest level, you can call execute( ) to run a (database-dependent) SQL statement. The return value depends on the database adapter being used. For MySQL, for example, it returns a Mysql::Result object. If you really need to work down at this low level, you’d probably need to read the details of this call from the code itself. Fortunately, you shouldn’t have to, as the database adapter layer provides a higher-level abstraction. 6 Inches, of course, are also a legacy unit of measure, but let’s not fight that battle here. Report erratum [...]... reference to table named with the plural form of xxx (page 2 16) xxx_count Maintains a counter cache for the child table xxx (page 235) position The position of this row in a list if acts_as_list is used (page 243) parent_id A reference to the id of this row’s parent if acts_as_tree is used (page 245) Report erratum 277 Chapter 16 Action Controller and Rails Action Pack lies at the heart of Rails applications... Controller, Action View, and Active Record handle the processing of requests, and the Rails environment knits them together into a coherent (and easy-to-use) whole For that reason, we’ll describe Action Controller in the context of Rails Let’s start by looking at the overall context of a Rails application 16. 1 Context and Dependencies Rails handles many configuration dependencies automatically; as a developer... and how it works within Rails In the next chapter, we’ll take on ActionView When we looked at Active Record, we treated it as a freestanding library; you can use Active Record as a part of a nonweb Ruby application Action Pack is different Although it is possible to use it directly as a framework, you probably won’t Instead, you’ll take advantage of the tight integration offered by Rails Components... a number of column names that have special significance to Active Record Here’s a summary Report erratum 2 76 M ISCELLANY created_at, created_on, updated_at, updated_on Automatically updated with the timestamp (_at form) or date (_on form) of a row’s creation or last update (page 267 ) lock_version Rails will track row version numbers and perform optimistic locking if a table contains lock_version (page... How does it route these requests to the appropriate code? Rails encodes this information in the request URL and uses a subsystem called routing to determine what should be done with that request The actual process is very flexible, but at the end of it Rails has determined the name of the controller that handles this particular request, along with a list of any other request parameters Typically one... You can use an xxx _url method wherever Rails expects URL parameters Thus you could redirect to the index page with the following code redirect_to(index_url) In a view template, you could create a hyperlink to the index using 16. 4 Action Methods When a controller object processes a request, it looks for a public instance method with the same name as the incoming action... following code adds method_missing( ) to a controller if the application is running in development mode If the controller is called with an invalid action, this renders an inline template to display the action’s name and a formatted version of the request parameters class SomeController < ApplicationController if RAILS_ ENV == "development" def method_missing(name, *args) render(:inline => %{ Unknown action:... S ESSIONS 16. 5 Cookies and Sessions Cookies allow web applications to get hash-like functionality from browser sessions: you can store named strings on the client browser and retrieve the values by name on subsequent requests This is significant because HTTP, the protocol used between browsers and web servers, is stateless Cookies provide a means for overcoming this limitation, allowing web applications... the lower-level ones By overriding the year in this example we implicitly tell the mapping code that we don’t need a month and day 2 This is natural on the web, where static content is stored within folders (directories), which themselves may be within folders, and so on Report erratum 285 R OUTING R EQUESTS Note also that the mapping code chose the first rule that could reasonably be used to render... the mix using the helper declaration described in Section 17.4, Helpers, on page 332 This is 16. 2 The Basics At its simplest, a web application accepts an incoming request from a browser, processes it, and sends a response The first question that springs to mind is, how does the application know what to do with the incoming request? A shopping cart application will receive requests to display a catalog, . describe Action Controller in the context of Rails. Let’s start by looking at the overall context of a Rails application. 16. 1 Context and Dependencies Rails handles many configuration dependencies. and e-mail address are obscured by our industrial-strength encryption. ar> mysql -urailsuser -prailspw railsdb mysql> select * from orders; + + + + + + + | id | name | email | address |. updated_at, updated_on Automatically updated with the timestamp (_at form) or date (_on form) of a row’s creation or last update (page 267 ). lock_version Rails will track row version numbers and