AntiPattern: Wet Validations
Ruby on Rails generally treats a database as a dumb storage device, essentially working only with many of the common-denominator features found in all the databases it supports and eschewing additional database functionality such as foreign keys and constraints. But many Rails developers eventually realize that a database has this functionality built in, and they attempt to use it by trying to duplicate the validation and constraints from their models into the database. For example, the following User model has a number of validations:
class User < ActiveRecord::Base validates :account_id, :presence => true validates :first_name, :presence => true validates :last_name, :presence => true validates :password, :presence => true, :confirmation => true, :if => :password_required? validates :email, :uniqueness => true, :format => { :with => %r{.+@.+\..+} }, :presence => true belongs_to :account end
You could attempt to create a database table to back this model that attempts to enforce the same validations at the database level, using database constraints. The (inadequate) migration to create that table might look something like this:
self.up create_table :users do |t| t.column :email, :string, :null => false t.column :first_name, :string, :null => false t.column :last_name, :string, :null => false t.column :password, :string t.column :account_id, :integer end execute "ALTER TABLE users ADD UNIQUE (email)" execute "ALTER TABLE users ADD CONSTRAINT user_constrained_by_account FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE" end self.down execute "ALTER TABLE users DROP FOREIGN KEY user_constrained_by_account" drop_table :users end
However, there are several reasons this doesn’t work in practice. For one thing, not all databases support all the constraints that Active Record supports. For example, in MySQL, it’s possible to enforce the uniqueness constraints on email, but none of the other constraints are fully possible without the use of stored procedures and triggers. For example, in the migration earlier in this chapter, there is only a constraint on NULL values in the first_name column. A blank string would still be allowed to be inserted.
If you are on a database that supports these constraints, you are then left to maintain them all by hand, in duplicate—a process that is tedious and error prone.
Active Record does not handle violations of database constraints well. It does not automatically read the constraints in the database. And if something is out of sync and a constraint in the database is hit, this will result in an exception that is not handled gracefully at the library level. The result is a failure the user sees or one that the programmer must handle, which is impractical.
Solution: Eschew Constraints in the Database
It’s simply best to not fight the opinion of Active Record that database constraints are declared in the model and that the database should simply be used as a datastore.
Despite all of the above, you may find yourself working with a DBA who insists that foreign key constraints or other constraints be stored in the database, or you yourself may simply believe in this principle. In such a case, it is strongly recommended that you not attempt to do this by hand and instead use a plugin that provides support for this. One such plugin is Foreigner (http://github.com/matthuhiggins/foreigner/), which provides support for managing foreign key constraints in migrations. Several other well-supported plugins provide support for additional constraints, most of which will be specific to your database server.
There’s Always an Exception
In the example we’ve been looking at in this section, the exception is NULL constraints coupled with default database values. Active Record handles these constraints perfectly, with the defaults even being picked up and populated in your model automatically. Therefore, the recommended way to provide default values to your model attributes is by storing the default values in the database. For example, if you want to default a Boolean column to true, you can do so in the database:
add_column :users, :active, :boolean, :null => false, :default => true
This will result in the active attribute on the user model being set to true whenever a new user is created:
>> user = User.new >> user.active? => true
You can use this swell behavior to your benefit to simplify code and make your objects more consistent. In most applications, setting all Booleans to allow null and to default to false is preferred. That way, your Booleans will really have only two possible values, true and false, not true, false, and nil.