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

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