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

ADVANCED ATTRIBUTES 380

Sometimes this convention breaks down. When it does, the observer class can explicitly list the model or models it wants to observe using the observe method.

Download e1/ar/observer.rb

class AuditObserver < ActiveRecord::Observer

observe Order, Payment, Refund

def after_save(model)

model.logger.info("#{model.class.name} #{model.id} created") end

end

AuditObserver.instance

In both these examples we’ve had to create an instance of the observer—merely defining the observer’s class does not enable that observer. For stand-alone Active Record applications, you’ll need to call the instance method at some convenient place during initialization. If you’re writing a Rails application, you’ll instead use the observer directive in your controller.

class StoreController < ApplicationController

observer :stock_control_observer

# ...

By convention, observer source files live in app/models.

In a way, observers bring to Rails much of the benefits of first-generation aspect-oriented programming in languages such as Java. They allow you to inject behavior into model classes without changing any of the code in those classes.

19.3Advanced Attributes

Back when we first introduced Active Record, we said that an Active Record object has attributes that correspond to the columns in the database table it wraps. 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

Report erratum

ADVANCED ATTRIBUTES 381

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;

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

Report erratum

ADVANCED ATTRIBUTES 382

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, because 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

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

Report erratum

TRANSACTIONS 383

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 method read_attribute 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.3 In our application we’d rather deal with inches,4 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

19.4Transactions

A database transaction groups a series of changes together in such a way that either all the changes are applied or none of the changes are applied. The classic example of the need for transactions (and one used in Active Record’s own documentation) is transferring money between two bank accounts. The basic logic is simple.

account1.deposit(100)

account2.withdraw(100)

3. A cubit is defined as the distance from your elbow to the tip of your longest finger. Because 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).

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

Report erratum

TRANSACTIONS 384

However, we have to be careful. What happens if the deposit succeeds but for some reason the withdrawal fails (perhaps the customer is overdrawn)? We’ll have added $100 to the balance in account1 without a corresponding deduction from account2. In effect we’ll have created $100 out of thin air.

Transactions to the rescue. A transaction is something like the Three Musketeers with their motto “All for one and one for all.” Within the scope of a transaction, either every SQL statement succeeds or they all have no effect. Putting that another way, if any statement fails, the entire transaction has no effect on the database.5

In Active Record we use the transaction method to execute a block in the context of a particular database transaction. At the end of the block, the transaction is committed, updating the database, unless an exception is raised within the block, in which case all changes are rolled back and the database is left untouched. Because transactions exist in the context of a database connection, we have to invoke them with an Active Record class as a receiver. Thus we could write

Account.transaction do account1.deposit(100) account2.withdraw(100)

end

Let’s experiment with transactions. We’ll start by creating a new database table. (Make sure your database supports transactions, or this code won’t work for you.)

Download e1/ar/transactions.rb

create_table :accounts, :force => true do |t| t.column :number, :string

t.column :balance, :decimal, :precision => 10, :scale => 2, :default => 0 end

Next, we’ll define a simple bank account class. This class defines instance methods to deposit money to and withdraw money from the account. It also provides some basic validation—for this particular type of account, the balance can never be negative.

Download e1/ar/transactions.rb

class Account < ActiveRecord::Base

def withdraw(amount) adjust_balance_and_save(-amount)

end

5. Transactions are actually more subtle than that. They exhibit the so-called ACID properties: they’re Atomic, they ensure Consistency, they work in Isolation, and their effects are Durable (they are made permanent when the transaction is committed). It’s worth finding a good database book and reading up on transactions if you plan to take a database application live.

Report erratum

TRANSACTIONS 385

def deposit(amount) adjust_balance_and_save(amount)

end

private

def adjust_balance_and_save(amount) self.balance += amount

save! end

def validate # validation is called by Active Record errors.add(:balance, "is negative") if balance < 0

end end

Let’s look at the helper method, adjust_balance_and_save. The first line simply updates the balance field. The method then calls save! to save the model data. (Remember that save! raises an exception if the object cannot be saved—we use the exception to signal to the transaction that something has gone wrong.)

So now let’s write the code to transfer money between two accounts. It’s pretty straightforward.

Download e1/ar/transactions.rb

peter = Account.create(:balance => 100, :number => "12345") paul = Account.create(:balance => 200, :number => "54321")

Download e1/ar/transactions.rb

Account.transaction do paul.deposit(10) peter.withdraw(10)

end

We check the database, and, sure enough, the money got transferred.

mysql>

select

* from accounts;

+----

 

+--------

 

+---------

 

+

| id

| number

| balance |

+----

 

+--------

 

+---------

 

+

|

5

| 12345

|

90.00

|

|

6

| 54321

|

210.00

|

+----

 

+--------

 

+---------

 

+

Now let’s get radical. If we start again but this time try to transfer $350, we’ll run Peter into the red, which isn’t allowed by the validation rule. Let’s try it.

Download e1/ar/transactions.rb

peter = Account.create(:balance => 100, :number => "12345") paul = Account.create(:balance => 200, :number => "54321")

Report erratum

TRANSACTIONS 386

Download e1/ar/transactions.rb

Account.transaction do paul.deposit(350) peter.withdraw(350)

end

When we run this, we get an exception reported on the console.

.../validations.rb:736:in ‘save!': Validation failed: Balance is negative from transactions.rb:46:in ‘adjust_balance_and_save'

: : :

from transactions.rb:80

Looking in the database, we can see that the data remains unchanged.

mysql>

select

* from accounts;

+----

 

+--------

 

+---------

 

+

| id

| number

| balance |

+----

 

+--------

 

+---------

 

+

|

7

| 12345

|

100.00

|

|

8

| 54321

|

200.00

|

+----

 

+--------

 

+---------

 

+

However, there’s a trap waiting for you here. The transaction protected the database from becoming inconsistent, but what about our model objects? To see what happened to them, we have to arrange to intercept the exception to allow the program to continue running.

Download e1/ar/transactions.rb

peter = Account.create(:balance => 100, :number => "12345") paul = Account.create(:balance => 200, :number => "54321")

Download e1/ar/transactions.rb

begin

Account.transaction do paul.deposit(350) peter.withdraw(350)

end rescue

puts "Transfer aborted" end

puts "Paul has #{paul.balance}" puts "Peter has #{peter.balance}"

What we see is a little surprising.

Transfer aborted

Paul has 550.0

Peter has -250.0

Although the database was left unscathed, our model objects were updated anyway. This is because Active Record wasn’t keeping track of the before and after states of the various objects—in fact it couldn’t, because it had no easy

Report erratum

TRANSACTIONS 387

way of knowing just which models were involved in the transactions. We can rectify this by listing them explicitly as parameters to the transaction method.

Download e1/ar/transactions.rb

peter = Account.create(:balance => 100, :number => "12345") paul = Account.create(:balance => 200, :number => "54321")

Download e1/ar/transactions.rb

begin

Account.transaction(peter, paul) do paul.deposit(350) peter.withdraw(350)

end rescue

puts "Transfer aborted" end

puts "Paul has #{paul.balance}" puts "Peter has #{peter.balance}"

This time we see the models are unchanged at the end.

Transfer aborted

Paul has 200.0

Peter has 100.0

We can tidy this code a little by moving the transfer functionality into the Account class. Because a transfer involves two separate accounts, and isn’t driven by either of them, we’ll make it a class method that takes two account objects as parameters. Notice how we can simply call the transaction method inside the class method.

Download e1/ar/transactions.rb

class Account < ActiveRecord::Base def self.transfer(from, to, amount)

transaction(from, to) do from.withdraw(amount) to.deposit(amount)

end end

end

With this method defined, our transfers are a lot tidier.

Download e1/ar/transactions.rb

peter = Account.create(:balance => 100, :number => "12345") paul = Account.create(:balance => 200, :number => "54321")

Download e1/ar/transactions.rb

Account.transfer(peter, paul, 350) rescue puts "Transfer aborted"

puts "Paul has #{paul.balance}" puts "Peter has #{peter.balance}"

Report erratum

TRANSACTIONS 388

Transfer aborted

Paul has 200.0

Peter has 100.0

There’s a downside to having the transaction code recover the state of objects automatically—you can’t get to any error information added during validation. Invalid objects won’t be saved, and the transaction will roll everything back, but there’s no easy way of knowing what went wrong.

Built-in Transactions

When we discussed parent and child tables, we said that Active Record takes care of saving all the dependent child rows when you save a parent row. This takes multiple SQL statement executions (one for the parent and one each for any changed or new children). Clearly this change should be atomic, but until now we haven’t been using transactions when saving these interrelated objects. Have we been negligent?

Fortunately not. Active Record is smart enough to wrap all of the updates and inserts related to a particular save (and also the deletes related to a destroy) in a transaction; either they all succeed or no data is written permanently to the database. You need explicit transactions only when you manage multiple SQL statements yourself.

Multidatabase Transactions

How do you go about synchronizing transactions across a number of different databases in Rails?

The current answer is that you can’t. Rails doesn’t support distributed twophase commits (which is the jargon term for the protocol that lets databases synchronize with each other).

However, you can (almost) simulate the effect by nesting transactions. Remember that transactions are associated with database connections, and connections are associated with models. So, if the accounts table is in one database and users is in another, you could simulate a transaction spanning the two using something such as

User.transaction(user) do Account.transaction(account) do

account.calculate_fees user.date_fees_last_calculated = Time.now user.save

account.save end

end

This is only an approximation to a solution. It is possible that the commit in the users database might fail (perhaps the disk is full), but by then the commit

Report erratum

TRANSACTIONS 389

Application Database

id

123

 

 

123

 

123

name

Dave

 

Fred

Dave

pay_type

check

 

check

po

etc...

...

 

 

...

 

...

 

 

 

 

 

 

 

 

process 1

 

 

o = Order.find(123)

 

 

o.name= 'Fred'

 

 

 

 

 

 

 

 

o.save

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

process 2

 

 

o = Order.find(123)

 

 

o.pay_type = 'po'

 

 

 

 

 

 

 

 

o.save

 

 

 

 

 

 

 

 

 

 

 

Figure 19.2: Race Condition: Second Update Overwrites First

in the accounts database has completed and the table has been updated. This would leave the overall transaction in an inconsistent state. It is possible (if not pleasant) to code around these issues for each individual set of circumstances, but for now, you probably shouldn’t be relying on Active Record if you are writing applications that update multiple databases concurrently.

Optimistic Locking

In an application where multiple processes access the same database, it’s possible for the data held by one process to become stale if another process updates the underlying database row.

For example, two processes may fetch the row corresponding to a particular account. Over the space of several seconds, both go to update that balance. Each loads an Active Record model object with the initial row contents. At different times they each use their local copy of the model to update the underlying row. The result is a race condition in which the last person to update the row wins and the first person’s change is lost. This is shown in Figure 19.2.

One solution to the problem is to lock the tables or rows being updated. By preventing others from accessing or updating them, locking overcomes concurrency issues, but it’s a fairly brute-force solution. It assumes that something will go wrong and locks just in case. For this reason, the approach is often called pessimistic locking. Pessimistic locking is unworkable for web applications if you need to ensure consistency across multiple user requests, because

Report erratum

TRANSACTIONS 390

it is very hard to manage the locks in such a way that the database doesn’t grind to a halt.

Optimistic locking doesn’t take explicit locks. Instead, just before it writes updated data back to a row, it checks to make sure that no one else has already changed that row. In the Rails implementation, each row contains a version number. Whenever a row is updated, the version number is incremented. When you come to do an update from within your application, Active Record checks the version number of the row in the table against the version number of the model doing the updating. If the two don’t match, it abandons the update and throws an exception.

Optimistic locking is enabled by default on any table that contains an integer column called lock_version. You should arrange for this column to be initialized to zero for new rows, but otherwise you should leave it alone—Active Record manages the details for you.

Let’s see optimistic locking in action. We’ll create a table called counters containing a simple count field along with the lock_version column. (Note the :default setting on the lock_version column.)

Download e1/ar/optimistic.rb

create_table :counters, :force => true do |t|

t.column :count,

:integer

t.column :lock_version, :integer, :default => 0 end

Then we’ll create a row in the table, read that row into two separate model objects, and try to update it from each.

Download e1/ar/optimistic.rb

class Counter < ActiveRecord::Base end

Counter.delete_all

Counter.create(:count => 0)

count1 = Counter.find(:first) count2 = Counter.find(:first)

count1.count += 3 count1.save

count2.count += 4 count2.save

When we run this, we see an exception. Rails aborted the update of count2 because the values it held were stale.

Report erratum

TRANSACTIONS 391

/opt/local/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/locking.rb:47: in ‘update_without_callbacks': Attempted to update a stale object

(ActiveRecord::StaleObjectError)

If you use optimistic locking, you’ll need to catch these exceptions in your application.

You can disable optimistic locking with

ActiveRecord::Base.lock_optimistically = false

You can change the name of the column used to keep track of the version number on a per-model basis.

class Change < ActiveRecord::Base set_locking_column("generation_number")

# ...

end

Report erratum