Pushing Rails validations down to the database?
Ever open up a Rails console to debug a problem and come away wondering how the data got so funky? Despite our best efforts, the database will accept plenty of garbage data if you let it. There are tons of methods to bypass the Rails callbacks and validations while still updating your database. If you are like me you have probably used these methods in the Rails console to fix some of that funky data you found after some other code created it in the first place.
Active Record Validations are a good place to start to prevent the funk. They will check the attributes for presence, absence, numericality, length, inclusion, format, exclusion, and acceptance. You can validate your model’s data easily and prevent any number of common errors this way. They aren’t the end of the problem but for most funky data you see your mind should go to checking the validations and probably writing another one.
However, in a more complex app data may come in through an API or an Excel import that gets written directly to the database. This could be done without creating the models in the first place through an insert because the code author felt he knew what he was doing (when maybe he didn’t as well as he thought. It happens to the best of us.) The updates to the DB could happen after the models are loaded and updated via #update_all
or save(validate: false)
. In both cases, the code could be perfectly proper at the time and might even be well tested and might still cause a problem down the line. Software is soft and the models and their validations can change.
Sometimes you need to push the Rails validations down to the database. It is easy enough to do in a migration:
# Two simple ways to change the constraints of a db column.
change_column_null :books, :title, false # Prevents null
change_column_default :books, :published, from: true, to: false # Changes the default value
If you are unsure of if the database is currently enforcing these validations you can check in db/schema.rb:
# db/schema.rb
create_table "books", id: :integer, force: :cascade do |t|
t.string "name", null: false
t.boolean "published", default: false, null: false
t.string "blurb", limit: 3000
end
This ends up meaning that there are multiple places that check the validity of the data going into the database which may come off as you doing double the work. I am sure you, like me, got into coding to automate things and let the computer do double the work instead of you. However, this is ok seeing that Rails model validations also do more than just strict validation and can be used to pass up errors to the user and give them context about what was incorrect about their data entry. In contrast, database constraints often prevent the insertion of incorrect data resulting from a more programmatic method which means that you and your coworkers don’t trip yourselves up as much. So each can have a very different purpose despite their surface similarity.
To wrap up every validation doesn’t need to live on the database. Not every database constraint will provide value or even match perfectly with a model validation. The take-away tip for where to start is to go look through your db/schema.rb and find columns that should either be null: false or have a default value and go write migrations for those. They are easy and preventing nulls and having good defaults will save the trouble of wading through all kinds of bad data later.