Effective Validation and Data Integrity in Rails: A Comprehensive Approach

May 7, 2025

When building complex applications, ensuring that data is valid and consistent is a key aspect of maintaining integrity. In Rails, there are several ways to achieve this through validations, callbacks, and associations. In this article, I’ll walk you through a comprehensive example of how to implement robust data validation, as well as how to handle custom validation rules, track changes, and create detailed logs of modifications to your models.


Want to Improve the Validation in Your System?

If you’re looking to enhance the validation logic and data integrity in your Rails application, I’d love to help! Whether you’re working on custom validations, improving performance, or optimizing your data integrity workflows, let’s connect and explore solutions together. https://rubystacknews.com/get-in-touch/


1. The Power of Validations in Rails

Validations help us ensure that the data we store in our models meets certain criteria. Rails provides built-in validation helpers such as validates_presence_of, validates_uniqueness_of, and more. Custom validations are also possible, allowing you to enforce specific business rules.

Uniqueness Validation

One common use case is ensuring that records remain unique within a given context. For example, we may want to ensure that there are no duplicate entries for a specific combination of two fields:

validates_uniqueness_of :field_one, scope: :field_two

This validation ensures that the combination of field_one and field_two is unique within the database.

Custom Validation

Article content

Sometimes, we need to perform more complex validation. For instance, we may want to verify that a field is answerable based on its type. Here’s an example of a custom validation:

validate :field_must_be_answerable

def field_must_be_answerable
  return unless self.field
  unless ['type_1', 'type_2'].include?(self.field.field_type)
    errors.add(:field, 'is not answerable')
  end
end

In this case, we check whether the field_type is either type_1 or type_2, and if not, we add an error to the model.

2. Callbacks: Automating Data Manipulation

Callbacks allow us to run logic before or after certain events, such as creating or updating a record. They provide a way to modify data automatically or perform additional actions.

Before Save Callbacks

One of the most common use cases for before_save callbacks is cleaning or modifying data before it’s saved to the database:

before_save :clean_numeric_values

def clean_numeric_values
  return unless self.field_type == 'numeric'
  self.value.gsub!(/[^0-9.]/, '') if self.value
  self.value.gsub!(/^\./, '0.') if self.value
end

This callback ensures that only numeric values are stored in the value field, and any extraneous characters are removed.

After Save Callbacks

In some cases, we want to perform actions after a record has been successfully saved. For example, we can create a history entry to track changes made to the record:

after_save :create_history, unless: :field_is_special?

def create_history
  return unless self.changed?
  History.create(
    value: self.value,
    related_object: self
  )
end

This callback runs after the record is saved and creates a History record only if the value has changed.

3. Handling Associations in Validation and Logic

Associations between models are crucial in building relationships between records. You can validate the integrity of these associations and perform additional logic based on related models.

Validation Based on Associated Records

In this example, we perform validation based on related models:

def validate_answer?
  validation_result = true
  ValidationRule.where(field: self.field).each do |rule|
    validation_result = !eval(rule.expression)
    if validation_result
      ValidationError.where(answer_id: self.id, validation_rule_id: rule.id).delete_all
    else
      create_validation_error(rule)
    end
  end
  validation_result
end

Here, we evaluate a validation rule using eval and create a validation error if the rule fails. This approach allows the flexibility of defining validation rules dynamically.

4. Tracking Changes and History

Tracking changes to records is essential for auditing purposes, especially in systems that need to maintain a history of modifications. By leveraging callbacks, we can automatically create history records whenever a change occurs.

def create_history
  return unless self.changed?
  History.create(
    value: self.value,
    user: current_user,
    related_object: self
  )
end

In this example, a history entry is created every time the value changes, capturing the user who made the change and the new value.

5. Summing Subtotals

In more complex scenarios, you may need to calculate values dynamically based on related records. For instance, here’s how we can sum values from related records and update a subtotal field:

def calculate_subtotal
  return 0 unless is_subtotal_line?

  answers = Answer.where(
    field_id: self.field.subtotal_sources.pluck('id')
  ).map { |answer| answer.value.to_f.round }
  
  self.value = answers.sum
end

In this example, we sum the values of answers from related fields to calculate a subtotal.


Conclusion

By combining validations, callbacks, and associations, we can create robust, maintainable models that ensure data integrity and automate critical processes. In complex applications, it’s essential to use these tools effectively to handle validation, track changes, and perform actions based on related models. The flexibility provided by Rails enables you to tailor your logic to meet your business requirements while maintaining clean, efficient code.

Article content

Leave a comment