Using Transactions with Active Record

Mar 06, 2015 on ActiveRecord SQL SoftwarePatterns CleanCode MVC

A transaction is a database feature where you can create, update, or delete multiple records from a database and if just one of them fails, everything rolls back. For example, Postgres would roll everything back if there was some type of failure. ActiveRecord also provides support for this by allowing you to rollback if it is told to rollback.

In a previous post, I described why you would want to use service objects to encapsulate controller logic using an AirBnB style example application. Inside of this particular service object is exactly where you would want to use a transaction.

class ReserveListing
  def initialize(tenant, landlord, residence, date_range)
    @tenant = tenant
    @landlord = landlord
    @residence = residence
    @start_time = date_range.start_time
    @end_time = date_range.end_time
  end

  def book
    ActiveRecord::Base.transaction do
      if residence_unavailable?
        raise ActiveRecord::Rollback
      end
      remove_time_from_residence
      create_reservation
    end
  end
end

When a user reserves a residence, the model is responsible for checking if it is available, and if it isn't, rollback the transaction. You can imagine that these very broad methods that the service object is using can be really complex, especially for a controller. Also, it wouldn't fit in either of those ActiveRecord models because each model would become dependent on the other. with Another thing to note is in the above example, the conditional if residence_unavailable? method can trigger this raise ActiveRecord::Rollback exception. Normally you don't want to explicitly raise an exception in your code, but whenever you raise ActiveRecord::Rollback within a transaction, everything is cancelled and you can then handle what happens next.

Next, you may have to retrieve other information from this particular service object, such as sending out notifications. When you do this, you will need access to the reservation in this case. An easy way to gain access is to have a getter method and return self in the transaction.

class ReserveListing
  attr_reader :reservation
  ...
  def book
    ActiveRecord::Base.transaction do
      if residence_unavailable?
        raise ActiveRecord::Rollback
      end
      remove_time_from_residence
      create_reservation
      self
    end
  end
end

Now, inside of the controller action, you can then handle that reservation and pass it off to the Notification model, which is another service object to handle notifications.

class ReserveListing
  ...
  def send_notifications
    Notification.new(self).broadcast
    # code that sends out notifications (email, text message, etc.)
  end
  ...
end

Because you are returning self from #book, you can then call #send_notifications easily from the object.

One thing to note about transactions is that if it fails, it returns nil, which is actually perfect for this particular pattern.

class ReservationsController < ApplicationController
  def create
    ...
    reserve_listing = ReserveListing.new(
      tenant,
      landlord,
      residence,
      date_range
    ) || NullReserveListing.new
    ...
  end
end

Here we are using the Null Object Pattern, which I discussed in a previous post. All you have to do is implement the public methods that get triggered on ReserveListing which will be very simple as you will just return nil.

class NullReserveListing
  def reservation
  end
end

Now when you go to handle the redirect or response in reservations#create you can check the truthiness of the reservation getter method on whatever type of object reserve_listing is and this will tell you if your transaction succeeded or failed.

class ReservationsController < ApplicationController
  def create
    ...
    if reserve_listing.reservation
      reserve_listing.send_notifications
      render "success"
    else
      render "error"
    end
  end
end

Working with transactions can be difficult at first, but with a little work, you can leverage ActiveRecord's behavior to make your code very easy to come back to.

Comments

Add a comment: