Rails 7.1 Introduces ActiveRecord::Base::Normalization

Using ActiveRecord::Base::Normalization we can specify unique normalization rules for model attributes. When an attribute is assigned or changed, normalization is applied, and the normalised value is stored in the database.

To enable records to be queried using unnormalized values, the normalization is also done to the matching keyword argument of query methods. These rules are applied during data assignment and updates, ensuring that data is stored in a consistent and standardized format in the database.

Before Rails 7.1, we could normalize attributes using before_save callback.

Before

class User < ApplicationRecord
  before_save :normalize_name, if name.present?

  private

  def normalize_name
    self.name = name.strip.titlecase
  end
end

With our own custom normalization, we cannot make queries using finder methods using unnormalized values.

After

class User < ActiveRecord::Base
  normalizes :name, with: -> name { name.strip.titlecase }
end

Thanks to this PR, in the above code, The normalizes method is used to define an attribute normalization rule for :name attribute of the User model, it strips leading,
and trailing spaces and converts the name to titlecase.

By default, Normalization will not be applied to nil values. To normalize nil value, we can enable it using :apply_to_nil option.

user = User.create(name: " JOHN DOE \n")
user.name # => "John Doe"

# This demonstrates that the name attribute is successfully normalized during the creation of a new user.

We can apply the normalization rule manually using the class method normalize_value_for.

User.normalize_value_for(:name, " JOHN DOE\n")
# => "John Doe"

Normalization is by default applied to the finder method. This allows a record to be created and later queried using unnormalized values but it won’t work with SQL fragments. Since SQL does not automatically apply the normalization rules defined in our model, it searches for an exact match.

# Finding a user by a differently formatted name:
user = User.find_by(name: "\tJOHN DOE ")
user.name # => "John Doe"
user.name_before_type_cast # => "John Doe"


# Counting users based on a differently formatted name:
User.where(name: "\t JOHN DOE ").count # => 1
User.where(["name = ?", "\tJOHN DOE "]).count # => 0


# Checking for the existence of a user with a differently formatted name:
User.exists?(name: "\tJOHN DOE ") # => true
User.exists?(["name = ?", "\tJOHN DOE "]) # => false

Normalization is not automatically applied when fetching data from the database to prevent unintended alterations to pre-existing records. To apply normalization to pre-existing data, we can either assign a new value to the attribute or explicitly migrate it using the Normalization#normalize_attribute method.

legacy_user = User.find(1)
legacy_user.name # => " JOHN DOE\n"
legacy_user.normalize_attribute(:name)
legacy_user.name # => "John Doe"
legacy_user.save

Need help on your Ruby on Rails or React project?

Join Our Newsletter