- •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
AGGREGATION AND STRUCTURED DATA 313
Deleting Rows
Active Record supports two styles of row deletion. First, it has two class-level methods, delete and delete_all, that operate at the database level. The delete method takes a single id or an array of ids and deletes the corresponding row(s) in the underlying table. delete_all deletes rows matching a given condition (or all rows if no condition is specified). The return values from both calls depend on the adapter but are typically the number of rows affected. An exception is not thrown if the row doesn’t exist prior to the call.
Order.delete(123)
User.delete([2,3,4,5])
Product.delete_all(["price > ?", @expensive_price])
The various destroy methods are the second form of row deletion provided by Active Record. These methods all work via Active Record model objects.
The destroy instance method deletes from the database the row corresponding to a particular model object. It then freezes the contents of that object, preventing future changes to the attributes.
order = Order.find_by_name("Dave" ) order.destroy
# ... order is now frozen
There are two class-level destruction methods, destroy (which takes an id or an array of ids) and destroy_all (which takes a condition). Both read the corresponding rows in the database table into model objects and call the instancelevel destroy method of that object. Neither method returns anything meaningful.
|
30.days.ago |
Order.destroy_all(["shipped_at < ?", 30.days.ago]) |
֒→ page 252 |
|
Why do we need both the delete and the destroy class methods? The delete methods bypass the various Active Record callback and validation functions, while the destroy methods ensure that they are all invoked. (We talk about callbacks starting on page 373.) In general it is better to use the destroy methods if you want to ensure that your database is consistent according to the business rules defined in your model classes.
17.6Aggregation and Structured Data
(This section contains material you can safely skip on first reading.)
Storing Structured Data
It is sometimes helpful to store attributes containing arbitrary Ruby objects directly into database tables. One way that Active Record supports this is by serializing the Ruby object into a string (in YAML format) and storing that
Report erratum
AGGREGATION AND STRUCTURED DATA 314
string in the database column corresponding to the attribute. In the schema, this column must be defined as type text.
Because Active Record normally maps a character or text column to a plain Ruby string, you need to tell Active Record to use serialization if you want to take advantage of this functionality. For example, we might want to record the last five purchases made by our customers. We’ll create a table containing a text column to hold this information.
Download e1/ar/dump_serialize_table.rb
create_table :purchases, :force => true do |t| t.column :name, :string
t.column :last_five, :text end
In the Active Record class that wraps this table, we’ll use the serialize declaration to tell Active Record to marshal objects into and out of this column.
Download e1/ar/dump_serialize_table.rb
class Purchase < ActiveRecord::Base serialize :last_five
# ...
end
When we create new Purchase objects, we can assign any Ruby object to the last_five column. In this case, we set it to an array of strings.
purchase = Purchase.new purchase.name = "Dave Thomas"
purchase.last_five = [ 'shoes', 'shirt', 'socks', 'ski mask', 'shorts' ] purchase.save
When we later read it in, the attribute is set back to an array.
purchase = Purchase.find_by_name("Dave Thomas") pp purchase.last_five
pp purchase.last_five[3]
This code outputs
["shoes", "shirt", "socks", "ski mask", "shorts"] "ski mask"
Although powerful and convenient, this approach is problematic if you ever need to be able to use the information in the serialized columns outside a Ruby application. Unless that application understands the YAML format, the column contents will be opaque to it. In particular, it will be difficult to use the structure inside these columns in SQL queries. For these reasons object aggregation using composition is normally the better approach to use.
Report erratum
AGGREGATION AND STRUCTURED DATA 315
Composing Data with Aggregations
Database columns have a limited set of types: integers, strings, dates, and so on. Typically, our applications are richer—we define classes to represent the abstractions of our code. It would be nice if we could somehow map some of the column information in the database into our higher-level abstractions in just the same way that we encapsulate the row data itself in model objects.
For example, a table of customer data might include columns used to store the customer’s name—first name, middle initials, and surname, perhaps. Inside our program, we’d like to wrap these name-related columns into a single Name object; the three columns get mapped to a single Ruby object, contained within the customer model along with all the other customer fields. And, when we come to write the model back out, we’d want the data to be extracted out of the Name object and put back into the appropriate three columns in the database.
customers
id |
|
|
|
|
|
Customer |
|
|
|
|
|
|
|
|
|
|
|
|
|
id |
credit_limit |
|
|
|
|
|
|
|
|
|
|
|
|
|
first_name |
} |
|
|
|
credit_limit |
|
|
|
|
|
|||
initials |
|
|
|
name |
||
Name |
|
|||||
|
|
|
||||
last_name |
|
|
first |
|
|
last_purchase |
|
|
|
|
|
||
last_purchase |
|
|
initials |
|
|
purchase_count |
|
|
|
|
|
||
purchase_count |
|
|
last |
|
|
Model |
|
|
|
|
|
|
|
|
|
|
|
|
This facility is called aggregation (although some folks call it composition—it depends on whether you look at it from the top down or the bottom up). Not surprisingly, Active Record makes it easy to do. You define a class to hold the data, and you add a declaration to the model class telling it to map the database column(s) to and from objects of the dataholder class.
The class that holds the composed data (the Name class in this example) must meet two criteria. First, it must have a constructor that will accept the data as it appears in the database columns, one parameter per column. Second, it must provide attributes that return this data, again one attribute per column. Internally, it can store the data in any form it needs to use, just as long as it can map the column data in and out.
For our name example, we’ll define a simple class that holds the three components as instance variables. We’ll also define a to_s method to format the full name as a string.
Report erratum
AGGREGATION AND STRUCTURED DATA 316
Download e1/ar/aggregation.rb
class Name
attr_reader :first, :initials, :last
def initialize(first, initials, last) @first = first
@initials = initials @last = last
end
def to_s
[ @first, @initials, @last ].compact.join(" ") end
end
Now we have to tell our Customer model class that the three database columns first_name, initials, and last_name should be mapped into Name objects. We do this using the composed_of declaration.
Although composed_of can be called with just one parameter, it’s easiest to describe first the full form of the declaration and show how various fields can be defaulted.
composed_of :attr_name, :class_name => SomeClass, :mapping => mapping
The attr_name parameter specifies the name that the composite attribute will be given in the model class. If we defined our customer as
class Customer < ActiveRecord::Base composed_of :name, ...
end
we could access the composite object using the name attribute of customer objects.
customer = Customer.find(123) puts customer.name.first
The :class_name option specifies the name of the class holding the composite data. The value of the option can be a class constant, or a string or symbol containing the class name. In our case, the class is Name, so we could specify
class Customer < ActiveRecord::Base composed_of :name, :class_name => Name, ...
end
If the class name is simply the mixed-case form of the attribute name (which it is in our example), it can be omitted.
The :mapping parameter tells Active Record how the columns in the table map to the attributes and constructor parameters in the composite object. The parameter to :mapping is either a two-element array or an array of two-
Report erratum
AGGREGATION AND STRUCTURED DATA 317
customers |
|
class Customer < ActiveRecord::Base |
||
|
||||
id |
|
composed_of :name, |
|
|
created_at |
|
:class_name => Name, |
|
|
|
:mapping => |
|
|
|
credit_limit |
|
|
|
|
|
[ [ :first_name, |
:first |
], |
|
|
|
|||
first_name |
|
|||
|
[ :initials, |
:initials ], |
||
initials |
|
|||
|
[ :last_name, |
:last |
] |
|
last_name |
|
|||
|
] |
|
|
|
last_purchase |
|
|
|
|
|
end |
|
|
|
purchase_count |
|
|
|
|
|
|
|
class Name
attr_reader :first, :initials, :last
def initialize(first, initials, last) @first = first
@initials = initials @last = last end
end
Figure 17.2: How Mappings Relate to Tables and Classes
element arrays. The first element of each two-element array is the name of a database column. The second element is the name of the corresponding accessor in the composite attribute. The order that elements appear in the mapping parameter defines the order in which database column contents are passed as parameters to the composite object’s initialize method. Figure 17.2 shows how the mapping option works. If this option is omitted, Active Record assumes that both the database column and the composite object attribute are named the same as the model attribute.
For our Name class, we need to map three database columns into the composite object. The customers table definition looks like this.
Download e1/ar/aggregation.rb
create_table :customers, :force => true do |t|
t.column :created_at, |
:datetime |
t.column :credit_limit, |
:decimal, :precision => 10, :scale => 2, :default => 100 |
t.column :first_name, |
:string |
t.column :initials, |
:string |
t.column :last_name, |
:string |
t.column :last_purchase, |
:datetime |
t.column :purchase_count, |
:integer, :default => 0 |
end |
|
The columns first_name, initials, and last_name should be mapped to the first, initials, and last attributes in the Name class.12 To specify this to Active Record, we’d use the following declaration.
12. In a real application, we’d prefer to see the names of the attributes be the same as the name of the column. Using different names here helps us show what the parameters to the :mapping option do.
Report erratum
|
AGGREGATION AND STRUCTURED DATA |
318 |
Download e1/ar/aggregation.rb |
|
|
class Customer < ActiveRecord::Base |
|
|
composed_of :name, |
|
|
:class_name => Name, |
|
|
:mapping => |
|
|
[ # database |
ruby |
|
[ :first_name, |
:first ], |
|
[ :initials, |
:initials ], |
|
[ :last_name, |
:last ] |
|
] |
|
|
end |
|
|
Although we’ve taken a while to describe the options, in reality it takes very little effort to create these mappings. Once done, they’re easy to use: the composite attribute in the model object will be an instance of the composite class that you defined.
Download e1/ar/aggregation.rb
name = Name.new("Dwight" , "D", "Eisenhower")
Customer.create(:credit_limit => 1000, :name => name)
customer = Customer.find(:first)
puts customer.name.first |
#=> Dwight |
|
||
puts |
customer.name.last |
#=> |
Eisenhower |
|
puts |
customer.name.to_s |
#=> |
Dwight |
D Eisenhower |
customer.name = Name.new("Harry", nil, "Truman") customer.save
This code creates a new row in the customers table with the columns first_name, initials, and last_name initialized from the attributes first, initials, and last in the new Name object. It fetches this row from the database and accesses the fields through the composite object. Finally, it updates the row. Note that you cannot change the fields in the composite. Instead you must pass in a new object.
The composite object does not necessarily have to map multiple columns in the database into a single object; it’s often useful to take a single column and map it into a type other than integers, floats, strings, or dates and times. A common example is a database column representing money: rather than hold the data in native floats, you might want to create special Money objects that have the properties (such as rounding behavior) that you need in your application.
We can store structured data in the database using the composed_of declaration. Instead of using YAML to serialize data into a database column, we can instead use a composite object to do its own serialization. As an example let’s revisit the way we store the last five purchases made by a customer. Previously, we held the list as a Ruby array and serialized it into the database as a YAML string. Now let’s wrap the information in an object and have that object
Report erratum
AGGREGATION AND STRUCTURED DATA 319
save the data in its own format. In this case, we’ll save the list of products as a set of comma-separated values in a regular string.
First, we’ll create the class LastFive to wrap the list. Because the database stores the list in a simple string, its constructor will also take a string, and we’ll need an attribute that returns the contents as a string. Internally, though, we’ll store the list in a Ruby array.
Download e1/ar/aggregation.rb
class LastFive
attr_reader :list
#Takes a string containing "a,b,c" and
#stores [ 'a', 'b', 'c' ]
def initialize(list_as_string) @list = list_as_string.split(/,/)
end
#Returns our contents as a
#comma delimited string def last_five
@list.join(',' ) end
end
We can declare that our LastFive class wraps the last_five database column.
Download e1/ar/aggregation.rb
class Purchase < ActiveRecord::Base composed_of :last_five
end
When we run this, we can see that the last_five attribute contains an array of values.
Download e1/ar/aggregation.rb
Purchase.create(:last_five => LastFive.new("3,4,5"))
purchase = Purchase.find(:first)
puts purchase.last_five.list[1] |
#=> 4 |
Composite Objects Are Value Objects
A value object is an object whose state may not be changed after it has been created—it is effectively frozen. The philosophy of aggregation in Active Record is that the composite objects are value objects: you should never change their internal state.
Report erratum