Another default_scope story
Rails default_scope is an ActiveRecord macro that allows you to set a default scope for all the operations on the model. This is useful to filter out records by default, such as soft deleted ones.
class User < ApplicationRecord
default_scope { where(deleted_at: nil) }
end
In this way, we can filter out soft deleted users from all queries.
User.all
# SELECT * FROM users WHERE deleted_at IS NULL
This is a powerful tool, but it can lead to unexpected behavior. If you Google it, you will find a lot of examples. This is what I found out the hard way. On a Friday. Afternoon.
The problem⌗
In our application, we use soft deletion on a number of models, including users. The Pallet model has a belongs_to association with the User model. This ensures that somebody had inspected the pallet before approving it.
class Pallet < ApplicationRecord
belongs_to :inspection_by, class_name: 'User', optional: true
validates_presence_of :inspection_by, if: -> { approved_at.present? }
end
Last Friday we had to approve a pallet, but that was not possible because of a validation error. The record was not valid, with the error message Inspection by can't be blank. But the inspection_by_id attribute was set. What was going on?
The solution⌗
I checked the user who inspected the pallet and found out that they were soft deleted after inspecting the pallet. The default_scope was filtering out the user, and the validation was failing because the user was not found. The solution was to change the validation in the Pallet model to check for the inspection_by_id attribute instead of using the association.
class Pallet < ApplicationRecord
...
validates_presence_of :inspection_by_id, if: -> { approved_at.present? }
end
This way the validation will pass if the inspection_by_id attribute is set, regardless of the user being soft deleted. That was safe enough for our use case since many validations ensure that the user is valid at the moment of inspection.
So after a nice Friday afternoon deploy to production, we were able to approve the pallet. And I was able to enjoy the weekend.