- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •Models, Views, and Controllers
- •Installing Rails
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Unix/Linux
- •Rails and Databases
- •Keeping Up-to-Date
- •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 A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B2: Add Page Decorations
- •Task C: Cart Creation
- •Sessions
- •More Tables, More Models
- •Iteration C1: Creating a Cart
- •Iteration C3: Finishing the Cart
- •Task D: Checkout!
- •Iteration D2: Show Cart Contents on Checkout
- •Task E: Shipping
- •Iteration E1: Basic Shipping
- •Task F: Administrivia
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Finishing Up
- •More Icing on the Cake
- •Task T: Testing
- •Tests Baked Right In
- •Testing Models
- •Testing Controllers
- •Using Mock Objects
- •Test-Driven Development
- •Running Tests with Rake
- •Performance Testing
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Active Support
- •Logging in Rails
- •Debugging Hints
- •Active Record Basics
- •Tables and Classes
- •Primary Keys and IDs
- •Connecting to the Database
- •Relationships between Tables
- •Transactions
- •More Active Record
- •Acts As
- •Aggregation
- •Single Table Inheritance
- •Validation
- •Callbacks
- •Advanced Attributes
- •Miscellany
- •Action Controller and Rails
- •Context and Dependencies
- •The Basics
- •Routing Requests
- •Action Methods
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Builder templates
- •RHTML Templates
- •Helpers
- •Formatting Helpers
- •Linking to Other Pages and Resources
- •Pagination
- •Form Helpers
- •Layouts and Components
- •Adding New Templating Systems
- •Introducing AJAX
- •The Rails Way
- •Advanced Techniques
- •Action Mailer
- •Sending E-mail
- •Receiving E-mail
- •Testing E-mail
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Securing Your Rails Application
- •SQL Injection
- •Cross-Site Scripting (CSS/XSS)
- •Avoid Session Fixation Attacks
- •Creating Records Directly from Form Parameters
- •Knowing That It Works
- •Deployment and Scaling
- •Picking a Production Platform
- •A Trinity of Environments
- •Iterating in the Wild
- •Maintenance
- •Finding and Dealing with Bottlenecks
- •Case Studies: Rails Running Daily
- •Appendices
- •Introduction to Ruby
- •Ruby Names
- •Regular Expressions
- •Source Code
- •Cross-Reference of Code Samples
- •Resources
- •Index
AGGREGATION 247
Normally you’d have some end-user functionality to create and maintain the category hierarchy. Here, we’ll just create it using code. Note how we manipulate the children of any node using the children attribute.
File 2 |
root |
= Category.create(:name => "Books") |
|
|
fiction |
= root.children.create(:name => |
"Fiction") |
|
non_fiction = root.children.create(:name => |
"Non Fiction") |
|
|
non_fiction.children.create(:name => "Computers") |
||
|
non_fiction.children.create(:name => "Science") |
||
|
non_fiction.children.create(:name => "Art History") |
||
|
fiction.children.create(:name => "Mystery") |
|
|
|
fiction.children.create(:name => "Romance") |
|
|
|
fiction.children.create(:name => "Science Fiction") |
Now that we’re all set up, we can play with the tree structure. We’ll use the same display_children( ) method we wrote for the acts as list code.
File 2 |
display_children(root) |
# Fiction, Non Fiction |
|
sub_category = root.children.first |
|
|
puts sub_category.children.size |
#=> 3 |
|
display_children(sub_category) |
#=> Mystery, Romance, Science Fiction |
|
non_fiction = root.children.find(:first, :conditions => "name = 'Non Fiction'") |
|
|
display_children(non_fiction) |
#=> Art History, Computers, Science |
|
puts non_fiction.parent.name |
#=> Books |
The various methods we use to manipulate the children should look familar: they’re the same as those provided by has_many. In fact, if we look at the implementation of acts_as_tree, we’ll see that all it does is establish both a belongs_to and a has_many attribute, each pointing back into the same table. It’s as if we’d written
class Category < ActiveRecord::Base belongs_to :parent,
:class_name => "Category"
has_many :children, |
|
|
:class_name |
=> |
"Category", |
:foreign_key => |
"parent_id", |
|
:order |
=> |
"name", |
:dependent |
=> |
true |
end
If you need to optimize the performance of children.size, you can establish a counter cache (just as you can with has_many). Add the option
:counter_cache => true to the acts_as_tree declaration, and add the column children_count to your table.
15.2 Aggregation
Database columns have a limited set of types: integers, strings, dates, and so on. Typically, our applications are richer—we tend to define classes
Prepared exclusively for Rida Al Barazi
Report erratum
AGGREGATION 248
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 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.
Prepared exclusively for Rida Al Barazi
Report erratum
AGGREGATION 249
File 3 |
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 element arrays. The first element of each two-element array is the name of
Prepared exclusively for Rida Al Barazi
Report erratum
AGGREGATION 250
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 15.2: How Mappings Relate to Tables and Classes
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 15.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.
File 6 |
create table customers ( |
|
|
|
id |
int |
not null auto_increment, |
|
created_at |
datetime |
not null, |
|
credit_limit |
decimal(10,2) default 100.0, |
|
|
first_name |
varchar(50), |
|
|
initials |
varchar(20), |
|
|
last_name |
varchar(50), |
|
|
last_purchase |
datetime, |
|
|
purchase_count |
int |
default 0, |
|
primary key (id) |
|
|
|
); |
|
|
The columns first_name, initials, and last_name should be mapped to the first, initials, and last attributes in the Name class.2 To specify this to Active Record, we’d use the following declaration.
2In 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.
Prepared exclusively for Rida Al Barazi
Report erratum
|
|
AGGREGATION |
251 |
File 3 |
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. |
|
|
File 3 |
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.
Back on page 196, we saw how we could use the serialize declaration to store structured data in the database. We can also do this 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
Prepared exclusively for Rida Al Barazi
Report erratum
|
AGGREGATION |
252 |
|
information in an object and have that object 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. |
|
File 3 |
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 column in the database.
File 3 |
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.
File 3 |
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.
This is not always directly enforceable by Active Record or Ruby—you could, for example, use the replace( ) method of the String class to change the value of one of the attributes of a composite object. However, should you do this, Active Record will ignore the change if you subsequently save the model object.
Prepared exclusively for Rida Al Barazi
Report erratum