Understanding ActiveRecord Transactions

This article will discuss database transactions and how to use them in Ruby on Rails. Transaction in a database means the state of the database will only change if all the statements in a transaction block succeed, otherwise all the statements will roll back and no state change will occur.

For an example of a case using this concept, consider an application where users reserve bus tickets online. The reservation will go through the following steps:

  1. Users will enter general reservation information like date, time, number of seats, etc.
  2. Users will enter their credit card details.
  3. The application will hit the third party, Stripe maybe, to charge the credit card.
  4. The application will record the charge from the user.
  5. The application will create an entry in the reservations table.
  6. The application will notify the user and the admin that a reservation has been created.

Here steps 3-6 need attention because steps 1 and 2 only involve getting information from the user, but step 3 onwards will change the state of the database.

Imagine step 3 doesn’t succeed because of not connecting to, or an exception from, the third party, but steps 4, 5, and 6 succeed, resulting in the creation of a reservation and notification but no charge to the user.

Among steps 3-6, if any one step causes problems and the other two succeed, it will cause an issue with the application and reservation system. Here’s a working example:

1
2
3
4
5
6
rails new transaction
rails generate model User name
rails generate scaffold Reservation booking_time:datetime seats:integer user:references
rails generate model Notification message
rails generate model Charge charge_time:datetime status:string
rails db:create db:migrate

Secure the database from inconsistent data by editing the create method in app/controllers/reservations_controller.rb.

1
2
3
4
5
6
7
8
def create
ActiveRecord::Base.transaction do
 @reservation = Reservation.create(reservation_params)
 Stripe::Charge.create(......) # call to third party
 Charge.create(charge_time: Time.now, status: 'paid')
 Notification.create(message: 'Booking made successfully..!!')
end
end

In this way, if one operation fails, the transaction will make sure none of the operations change the state of the database.

Rescuing from the transaction block can be accomplished like this:

1
2
3
4
5
6
7
8
9
10
def create
ActiveRecord::Base.transaction do
 @reservation = Reservation.create(reservation_params)
 Stripe::Charge.create(......) # call to third party
 Charge.create(charge_time: Time.now, status: 'paid')
 Notification.create(message: 'Booking made successfully..!!')
end
rescue ActiveRecord::RecordInvalid
 	puts "There was an error..!!"
end

This method helps to avoid errors in applications in many different types of situations.