Safer SQL: Using ActiveRecord Transactions

  • ActiveRecord’s transaction method takes a block, and will only execute the block and write to your database if no exceptions are raised.
  • You can defined the transaction method on any class that inherits from ActiveRecord::Base, and that transaction will open up a single new database connection.

1. Opening database connections

A transaction opens up a single database connection. This means that when we call the transaction method, the method can only be invoked on the current database connection. This is important to remember if our application writes to multiple database at once; for example, if our Order and our Vendor data lived in two different databases, we’d need to nest our transactions:

Order.transaction do
  Vendor.transaction do
    order.process
    user.charge
    vendor.add_sale
  end
end

It’s generally a good idea to avoid nested transactions, mostly because the relationship between parent and child transactions can get complicated. This is especially the case because rollbacks are contained inside of their transactions blocks.

2. Different classes, one transaction

Because transactions are bound to database connections, we can mix different types of models inside of a transaction block. In fact, that’s exactly what we were doing when we wrote our initial transaction:

Order.transaction do
  @order.process
  @user.charge
  @vendor.add_sale
end

3. Class and instance methods

The great part about transaction is that it is available to us as both a class and an instance method for our ActiveRecord models. What does this mean, exactly? Well, the short answer is that we can write a transaction is lots of different ways, since we can invoke the transaction method on a class or an instance.
For example, we could have written this:

User.transaction do
  # methods we want to call go here
end

Vendor.transaction do
  # methods we want to call go here
end

Or any of these:

@order.transaction do
end

@user.transaction do
end

@vendor.transaction do
end

And if we were writing a method inside of the Order, Vendor, or User classes, these options would have worked as well:

self.transaction do
end

self.class.transaction do
end

The key here is that the transaction can be called on any class that inherits from ActiveRecord::Base. Why is that the key? Well, you might remember that we initially started off wanting to write a transaction inside of our service object…right? In that case, we can’t use something like transaction do, because self is the service object class, which does not inherit from ActiveRecord::Base!
So, what do? Well, just call the transaction method on to ActiveRecord::Base directly! there’s a quick fix for that.

ActiveRecord::Base.transaction do
  # methods we want to call go here
end

4. Exceptions are the rule

There’s one golden rule of the transaction block: it will only rollback the transaction if an error is raised. Why is this important? Well, calling something like save or destroy inside of a transaction will not raise an error; if something goes wrong, these methods will simply return false. Which means that our transaction block will continue, since there was no error raised! Uh oh…how to fix? Just use the save! and destroy! methods instead! These are both ActiveRecord methods which raise an exception if they don’t execute successfully:

ActiveRecord::Base.transaction do
  @order.destroy!
  @user.save!
end

If we really, really wanted to use save instead of save!, we’d have to manually raise an error in the block for our transaction to work as expected.

Alexander M About 8 years ago