The Case for before_validation callback: complex state normalization
Few months ago I wrote a blog post about ActiveRecord before_validation
callback and how it is used for wrong reasons and concluded that in most cases this is not something we should be using routinely. However, I missed one appropriate use case for it which might be quite common in Rails apps, so this might be an excellent opportunity to get back to before_validation callback and show its other side.
Anatomy Of The Problem
Imagine that we have a Payment
model where we need to store amount
and currency
. However, for statistical purposes, we also want to store normalized amount in USD currency with exchange rate applied at the time of payment’s creation. As this is a significant part of our domain, we want to add validation for amount_in_usd
attribute. Our Payment model looks like this at the moment:
class Payment < ApplicationRecord
validates :amount, :currency, :amount_in_usd, presence :true
end
The question is: where do we get amount_in_usd
from and how can we assign it?
The Solution
One way of solving that problem would be a direct assignment when populating all the attributes. In that case, it would look a bit like this:
Payment.new(currency: currency, amount: amount, amount_in_usd: CurrencyExchanger.exchange(amount, from: currency, to: "USD"))
The problem with that solution is that this logic would need to be repeated in every place where payment gets initialized. We could implement a factory class that would be reused in all scenarios to keep it DRY, but that’s some extra overhead that is not popular in a Rails world. Also, this sounds like a responsibility of the Payment model itself as it is about managing its internal state.
Here, we can’t solve this by overriding writers as I suggested before as amount_in_usd
depends on two attributes: currency
and amount
, and we don’t know in which sequence the attributes will be assigned.
And this is exactly the case where before_validation
is useful: for complex state normalization where multiple attributes are involved. With that callback, a solution looks quite elegant and just simpler:
class Payment < ApplicationRecord
validates :amount, :currency, :amount_in_usd, presence :true
before_validation :assign_amount_in_usd
private
def assign_amount_in_usd
if currency && amount
self.amount_in_usd = CurrencyExchanger.exchange(amount, from: currency, to: "USD")
end
end
end
Alternative Solution
In the first paragraph, I mentioned that this solution could work especially well in Rails apps. What I meant by that is the fact that usually, the “primitive” attributes coming from HTTP params are mass-assigned to the model. Of course in Ruby, everything is an object, but to keep things simpler, let’s treat numeric types and strings as primitives.
What would be a non-primitive value though? In our case, we have something that is widely used as a typical example of a value object: Money object that is composed of amount
and currency
. If the attributes before the assignment were mapped to some more domain-oriented objects, we would have an even simpler solution for our problem:
money = Money.new(amount, currency)
Payment.new(money: money)
and the model would look like this:
class Payment < ApplicationRecord
validates :amount, :currency, :amount_in_usd, presence :true
def money=(money_object)
self.amount = money_object.amount
self.currency = money_object.currency
self.amount_in_usd = CurrencyExchanger.exchange_money(money_object, to: "USD")
end
end
It might look like extra overhead that is not necessary. However, value objects tend to simplify and DRY a lot of things in the code, so for more complex apps, using value objects will be worth that extra overhead.
Wrapping Up
There are some cases where before_validation
callback might be useful. However, in more complex apps, using value object might be an alternative worth looking into.