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
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.
Top comments (0)