Transactions

A database transaction groups a series of changes in such a way that either the database applies all of the changes or it applies none of the changes. 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 straightforward:

 account1.​deposit​(100)
 account2.​withdraw​(100)

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.

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 the database rolls back all of the changes. 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 this:

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

 create_table ​:accounts​, ​force: ​​true​ ​do​ |t|
  t.​string​ ​:number
  t.​decimal​ ​:balance​, ​precision: ​10, ​scale: ​2, ​default: ​0
 end

Next, we’ll define a rudimentary 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.

 class​ Account < ActiveRecord::Base
  validates ​:balance​, ​numericality: ​{​greater_than_or_equal_to: ​0}
 def​ ​withdraw​(amount)
  adjust_balance_and_save!(-amount)
 end
 def​ ​deposit​(amount)
  adjust_balance_and_save!(amount)
 end
 private
 def​ ​adjust_balance_and_save!​(amount)
  self.​balance​ += amount
  save!
 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:

 peter = Account.​create​(​balance: ​100, ​number: ​​"12345"​)
 paul = Account.​create​(​balance: ​200, ​number: ​​"54321"​)
 Account.​transaction​ ​do
  paul.​deposit​(10)
  peter.​withdraw​(10)
 end

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

 depot>​​ ​​sqlite3​​ ​​-line​​ ​​db/development.sqlite3​​ ​​"select * from accounts"
  id = 1
  number = 12345
 balance = 90
 
  id = 2
  number = 54321
 balance = 210

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:

 peter = Account.​create​(​balance: ​100, ​number: ​​"12345"​)
 paul = Account.​create​(​balance: ​200, ​number: ​​"54321"​)
 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:

 depot>​​ ​​sqlite3​​ ​​-line​​ ​​db/development.sqlite3​​ ​​"select * from accounts"
  id = 1
  number = 12345
 balance = 100
 
  id = 2
  number = 54321
 balance = 200

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:

 peter = Account.​create​(​balance: ​100, ​number: ​​"12345"​)
 paul = Account.​create​(​balance: ​200, ​number: ​​"54321"​)
 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 way of knowing just which models were involved in the transactions.

Built-In Transactions

When we discussed parent and child tables in Specifying Relationships in Models, 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, no. Active Record is smart enough to wrap all 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.

While we have covered the basics, transactions are actually very subtle. 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.

What We Just Did

We learned the relevant data structures and naming conventions for tables, classes, columns, attributes, IDs, and relationships. We saw how to create, read, update, and delete this data. Finally, we now understand how transactions and callbacks can be used to prevent inconsistent changes.

This, coupled with validation as described in Chapter 7, Task B: Validation and Unit Testing, covers all the essentials of Active Record that every Rails programmer needs to know. If you have specific needs beyond what is covered here, look to the Rails Guides[97] for more information.

The next major subsystem to cover is Action Pack, which covers both the view and controller portions of Rails.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.95.38