Presence, NULL, and Product Requirements in Rails

A short article why NOT NULL constraints are essential for long-term product health.

An issue reported against active_record_doctor made me revisit my reasoning behind certain active_record_doctor rules, and I decided to put that in writing. Let’s start with the technical notions of presence, absence, and NULL, and then discuss how they relate to product requirements.

Validators vs Constraints

Constraints are checks enforced by the database that are used for ensuring data consistency. One of those checks is marking a column as NOT NULL, which tells the database to disallow NULL values in that column. Attempting to insert a NULL will result in a database error that gets translated into ActiveRecord::NotNullViolation by Rails. I use database constraints a lot for three reasons:

  1. They cannot be circumvented, which makes them robust.
  2. As a form of executable documentation: they make it clear that a value will be present when reading, or will be required when writing.
  3. I see them as a way of enhancing testing by introducing small assertions that are checked throughout the entire test suite.

Presence and absence validators provided by Rails play a similar role, but are more focused on producing user-friendly error messages. Unfortunately, there are ways to skip them, which makes them less robust than database constraints. Additionally, being present means something more than not being nil. An empty string or a string consisting of white space are examples of objects that are not present, but are definitely not nil either.

That brief technical background is enough to discuss how these notions relate to product requirements.

Mapping Product Requirements to Engineering Concepts

Product requirements often mandate presence of specific inputs, like email addresses or names. The intent behind these requirements can be realized by using Rails presents validators. The primary reason for this is the fact that even when an input is left blank then the browser will still send it to the server as an empty string, so an actual NULL is nowhere to be seen, and NOT NULL would enforce absolutely nothing.

Theoretically, a presence validator is all we need. In practice, I find NOT NULL constraints to be equally important, even if they’re more lenient than presence validators. There are three reasons for that:

  1. It’s easy to create a test fixture with an unexpected NULL, resulting in the test suite running on unrealistic data.
  2. Operations like insert_all, upsert_all, and update_all skip validations, but maybe critical for performance.
  3. Similarly, it’s easy to end up with an unwanted NULL when changing the database structure in a complex migration.

That’s why my recommendation is: if you add a presence validator to a model then add a NOT NULL constraint on the underlying column. active_record_doctor enforces that rule via the missing_non_null_constraint check.

As a side note, I also recommend doing the inverse: adding a presence validator for each NOT NULL in the database. This is a topic for a separate article, though.

Closing Thoughts

In this article, we discussed why every presence validator would benefit from the corresponding NOT NULL constraint. That and many other rules are enforced by active_record_doctor, so give it a shot if you want to solve database issues before they hit production.

Enjoyed the article? Follow me on Twitter!

I regularly post about Ruby, Ruby on Rails, PostgreSQL, and Hotwire.

Leave your email to receive updates about articles.