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

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. The Column 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 correspond 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;

Prepared exclusively for Rida Al Barazi

Report erratum

ADVANCED ATTRIBUTES 273

If we manually run this query against our database, we might see something 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

Prepared exclusively for Rida Al Barazi

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

5A 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).

Prepared exclusively for Rida Al Barazi

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 connection 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.

6Inches, of course, are also a legacy unit of measure, but let’s not fight that battle here.

Prepared exclusively for Rida Al Barazi

Report erratum

MISCELLANY 276

The select_all( ) method executes a query and returns an array of attribute hashes corresponding to the result set.

res = Order.connection.select_all("select id, "+

"quantity*unit_price as total " +

"from line_items")

p res

This produces something like

[{"total"=>"29.95", "id"=>"91"}, {"total"=>"59.90", "id"=>"92"}, {"total"=>"44.95", "id"=>"93"}]

The select_one( ) method returns a single hash, derived from the first row in the result set.

Have a look at the RDoc for AbstractAdapter for a full list of the low-level connection methods available.

The Case of the Missing ID

There’s a hidden danger when you use your own finder SQL to retrieve rows into Active Record objects.

Active Record uses a row’s id column to keep track of where data belongs. If you don’t fetch the id with the column data when you use find_by_sql( ), you won’t be able to store the result back in the database. Unfortunately, Active Record still tries and fails silently. The following code, for example, will not update the database.

result = LineItem.find_by_sql("select quantity from line_items") result.each do |li|

li.quantity += 2 li.save

end

Perhaps one day Active Record will detect the fact that the id is missing and throw an exception in these circumstances. In the meantime, the moral is clear: always fetch the primary key column if you intend to save an Active Record object back into the database. In fact, unless you have a particular reason not to, it’s probably safest to do a select * in custom queries.

Magic Column Names

In the course of the last two chapters we’ve mentioned a number of column names that have special significance to Active Record. Here’s a summary.

Prepared exclusively for Rida Al Barazi

Report erratum

MISCELLANY 277

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 213).

type

Used by single table inheritance to track the type of a row (page 253).

id

Default name of a table’s primary key column (page 197).

xxx_id

Default name of a foreign key reference to table named with the plural form of xxx (page 216).

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).

Prepared exclusively for Rida Al Barazi

Report erratum