Matthew Lindfield Seager

Matthew Lindfield Seager

Validating Data in Rails: Database Constraints, Model Validations or Client-side Checks?

I stumbled across an old StackOverflow question today about when to use model validations as opposed to database constraints in Rails apps. The question was originally about Rails 3.1 but whether you’re using Rails 3 or Rails 6 the correct answer is, of course, it depends.

Both database constraints and model validations exist for a reason. In addition to those two, I think it’s also worth considering where client-side checks fit in.

My short answer is:

  • always write model validations
  • in most cases back them up with database constraints
  • sometimes add on client-side checks

As with most things, stick with these simple rules while you’re learning and you won’t go far wrong. Later on you can start to break them, once you know why you’re breaking them.

My longer (“it depends”) answer and my reasons for the rules above are below.

Model Validations

Model validations are your bread and butter as a Rails developer. They aren’t as close to the customer as client-side checks, requiring a round trip to the server to find out a form is invalid, but they are usually quick enough. They aren’t quite as robust as database constraints, but they are still quite powerful and they are much more user friendly.

Because they allow you to validate multiple requirements and present a consolidated error to the customer this is where you should start.

Their main downsides are that they don’t necessarily enforce data integrity (they lack robustness) and performance. To address the robustness concerns we layer on database constraints. If (and only if) you identify that model validations are detracting from the customer experience you can add client-side checks to improve the situation.

Database Constraints

Database constraints are lower level and much more robust. They aren’t very customer friendly when used on their own but should be added on top of (below?) model validations in most cases.

Whilst model validations are good (and not easily circumvented by customers), they are easy to circumvent by a developer which can lead to inconsistent data:

By contrast, database constraints are very difficult to accidentally circumvent by the customer or the developer (although a determined developer can deliberately circumvent them).

The sorts of data integrity issues that constraints can prevent include:

  • non-unique IDs from multiple users of your Rails app (see ‘race condition’ link above)
  • orphaned records when a parent record is deleted
  • duplicate “has_one” records
  • other invalid data from other apps or from developers skipping constraint checks

Add database constraints to most model validations as an additional layer of protection. If there is a bug in your app, it’s better for your app to throw up an ugly error to the user straight away than for it to quietly save invalid data that eventually causes new bugs or leads to data corruption/loss.

The main potential downside to constraints is that you might need to make them database specific in which case you’ll lose the abstraction benefits of ActiveRecord::Migration.

Client-side Checks

Client-side checks (e.g. using JavaScript or HTML 5’s required attribute - which came out after the question was asked) are the closest to the customer and helpful for providing immediate feedback without any round trip to the server.

Add these on top of model validations when you can do so tastefully^ and you need to improve the customer [user] experience. This is particularly important for customers on low-bandwidth or high-latency connections (e.g. developing markets, remote locations, poor mobile reception or plane/hotel/ship wifi).

Keep in mind they are very easy to circumvent, unintentionally (JavaScript is disabled/blocked) or deliberately (by editing the source). They should never be relied upon for data integrity.

^ Also keep in mind that they are very easy to misuse/abuse. Make sure you avoid anti-patterns such as showing errors on fields that haven’t been filled in yet or constantly flashing an email field red/green (invalid/valid) on every keystroke.

Caveats

  • There is some amount of repetition between the levels; some people might object to using more than one level on the grounds it isn’t DRY. I think this is one of those cases where a little bit of repetition increases application quality substantially
  • On that note, make sure all 3 levels (if you use them) are consistent:
    • Your customers will be annoyed if the client-side check gives their password a big green tick but then an error comes back from the model validation saying it’s too short
    • It’s possible to have passing model tests even though the data can’t be saved to the database (if your constraints don’t match your validations and you’ve mocked out ActiveRecord)
    • Raising a validation error on a hidden or calculated field that the customer can’t control is unhelpful at best and hugely frustrating at worst.

When Not to Use Model Validations

As alluded to above there are some situations where model validations should be omitted or removed and only a database constraint used. The main use case for this is when the validation is not helpful to (actionable by) the customer.

For example, imagine a registration form requires an email address and a password but the Person model requires email_address and hashed_password. If for some reason a bug causes hashed_password to be nil the form submission will fail with a model validation message saying “hashed password can not be blank”. This is confusing and unhelpful to the customer plus it potentially masks an actual bug in your code.

If you remove the model validation from hashed_password but keep the database constraint, the same attempt to register will cause an SQL error (which will then be trapped by your error reporting system). In this case it’s much clearer to the customer that there is a bug in the software (not just a problem with the password they’re registering) and they hopefully won’t retry the submission elventy bajillion times.