Home > Articles > Programming > Ruby

📄 Contents

  1. AntiPattern: Messy Migrations
  2. AntiPattern: Wet Validations
  • Print
  • + Share This
This chapter is from the book

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.

  • + Share This
  • 🔖 Save To Your Account