When you first start to learn Rails, you read about Model Validation, which consists of statements such as “validates_numericality_of :the_field”. This prevents non-conforming models from being saved. Properly leveraged, these help ensure business constraints with an iron fist (excepting validation override).
Where to Best Enforce Business Constraints
Web apps constantly process form inputs and do the following:
- Ensure inputs conform to business constraints (e.g., is the value only 1 to 60)
- Give the user feedback on non-conforming inputs to allow changes.
This validation is typically done at one of the following three levels:
- Controllers. This is the last place validation should be done, but it is often the most tempting. Keep controllers as lean as possible and place business logic in models to conform to MVC. If your system exposes an API, controller business logic will be missed or duplicated.
- Method within Model. This is an improvement. Controllers can invoke this method to ensure business constraints prior to the models being saved. Furthermore, the logic remains in the model. However, if the controller forgets to invoke this method, then the logic is missed.
- Model Validation Statement or Validation Method. This is the most iron-clad way to enforce system constraints. If they do not conform, they simply cannot be saved (unless you use the Rails option to ignore validation, of course).
In addition, the testing of the model business logic can be handled in the unit as opposed to functional tests.
When first moving existing validation logic into the third option, you should expect to spend some time updating previously less strict test data.
Sometimes you will run into problems with what I call phantom models, as follows:
- In response to multi-step user input, a blank phantom model instance is first created.
- After all appropriate inputs are received, this phantom model is updated.
- You later add validation(s) to the model.
- Step 1 no longer works because a blank model does not conform to the new validation.
- Thus, you need to either refactor, remove the validations, or add the blank model without validation being enforced (with the risks of invalid data in the database).
The control Rails gives you over generated error messages using declarations is also not ideal. Consider the following for a field named minutes_running:
:message => “must be a whole number between 0 and 60”
This will generate the following message:
“Minutes Running must be a whole number between 0 and 60”
Want a different message that does not begin with the database field name? I would love to know how.
What I like best about doing this at the model level is the declarative approach. You simply declare the constraints, which is simpler, faster than coding, and clearer later on to you and others. If not powerful enough, you can also write validation methods and declare them as being part of model validation.
One mistake I made was not realizing how flexible and configurable these declarative statements are. The following (trimmed) sample from a single model ends this article:
validates_presence_of :user_name validates_presence_of :run_count, :if => :can_process, # boolean entity in model, could also use boolean method :message => "must be specified if this can be processed" validates_numericality_of :minutes_exercising, :greater_than => -1, :less_than => 61, :only_integer => true, # no floating points allowed! :message => "must be a whole number between 0 and 60" validate :some_method_with_complex_validation?