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

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