It’s nothing new that ActiveRecord callbacks are abused in many projects and used for the wrong reasons for many use cases where they can be easily avoided in favor of a much better alternative, like service objects. There is one callback though that is special and quite often used for pretty exotic reasons that have nothing to do with the process when it gets executed - it’s the
Data formatting is something pretty common in the majority of the applications, especially stripping strings. Imagine that you need to strip some
URL so that potential spaces won’t cause any issues. How would you approach that?
One way would be to use
before_validate callback, especially if you have some format validations:
class MyModel before_validate :strip_url private def strip_url self.url = url.to_s.strip end end
It gets the job done. However, how would you test it? You would need to call
valid? method on the model to check that…
URL is stripped? Sounds quite funny and is even better when you look at the potential spec:
require "rails_helper" RSpec.describe MyModel, type: :model do it "strips URL before validation" do model = MyModel.new(url: " http://rubyonrails.org") model.valid? expect(model.url).to eq "http://rubyonrails.org" end end
It’s quite unlikely that this would be the result of TDD though ;). What’s the alternative then?
How about just using attribute writer for that? So something like this:
class MyModel def url=(val) super(val.to_s.strip) end end
And here is a potential spec for this feature:
require "rails_helper" RSpec.describe MyModel, type: :model do it "strips URL" do model = MyModel.new(url: " http://rubyonrails.org") expect(model.url).to eq "http://rubyonrails.org" end end
Both the implementation and spec are much simpler and just more natural - data formatting has nothing to do with the validation, there is no need to use a callback related to validation to handle such use case.
Populating attributes and relationships
Another popular scenario is assigning attributes and relationships. Imagine you are creating a comment with a
content, an author who will be
current_user and also want to do some denormalization for performance reasons and directly assign
group to this comment to which
current_user belongs to. Here is how it is sometimes handled with
Comment.create!( content: content, author: current_user, )
class MyModel before_validate :assign_group private def assign_group self.group = author.group if author end end
It’s quite similar to the previous use case with data formatting - to write a test for this feature, we would need again to call
valid? which doesn’t make much sense, validation has nothing to do with populating attributes or relationships. There is much simpler and much more explicit way to handle it:
Comment.create!( content: content, author: current_user, group: current_user.group, )
There is no magic here - just a simple assignment, which is easy to test and understand.
Maybe there are some scenarios where
before_validate callback is the best possible choice (I’m yet to find them though), but I’m pretty sure data formatting or populating attributes/associations are not valid cases to use it for.