Understanding reject_if in Ruby on Rails
I recently stumbled upon a Rails pitfall that led to unwanted records cluttering pur database, and while trying to fix it I stumbled upon reject_if
.
The Problem
My journey began with a User
model that has_one Group
. To make life easier, I used accepts_nested_attributes_for :group
, allowing me to create a user and its associated group in one go:
1
2
3
4
class User < ApplicationRecord
has_one :group, dependent: :destroy
accepts_nested_attributes_for :group
end
1
2
# How we create a new User:
User.create(email: "test@example.com", group_attributes: { name: "Test Group name" })
The idea was simple: if I provided group_attributes
, a new group should be created.
However, I soon noticed something. Sometimes we received data like User.create(email: "test@example.com", group_attributes: { name: nil })
. This came from an external web form submission that made a request to an endpoint in our Rails app. The field for group_name
was optional, so this was expected. The unexpected thing was that even with seemingly “empty” attributes for the group (name: nil
), a new group record was still being created.
My intention was to only create a group if there was meaningful data for it. It turns out that the presence of the empty hash in the group_attributes
was enough for Rails to trigger the creation, leading to unnecessary records in the database.
The Solution: reject_if
After some digging, I discovered the solution: the :reject_if
option for accepts_nested_attributes_for
. This option allows you to specify a condition under which Rails should not create or update the associated record, even if attributes are provided.
Here’s how I updated my User model:
1
2
3
4
class User < ApplicationRecord
has_one :group, dependent: :destroy
accepts_nested_attributes_for :group, reject_if: :all_blank
end
Rails provides a convenient :all_blank
symbol, which is perfect for this use case. By adding reject_if: :all_blank
, I told Rails: “If all the attributes provided for group are blank (nil), then just ignore them and don’t create a new Group.” If you need more granular control, you can also pass a Proc (or lambda) or the name of a method:
1
2
# Reject if the 'name' attribute is blank
accepts_nested_attributes_for :group, reject_if: lambda { |attributes| attributes['name'].blank? }
1
2
3
4
5
6
# Or using a method
accepts_nested_attributes_for :group, reject_if: :my_custom_rejection_logic
def my_custom_rejection_logic(attributes)
attributes['some_other_field'].blank? && attributes['another_field'].blank?
end
Conclusion
If you’re using accepts_nested_attributes_for
in your Rails applications, take a moment to consider if reject_if
could help you avoid unwanted record creations and keep your database tidy.