Post

Understanding reject_if in Ruby on Rails

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.

This post is licensed under CC BY-NC 4.0 by the author.