Your Rails application probably makes use of uniqueness validations in several key places. This validation provides for a nice user experience when duplicate records are detected but as we will see in a moment, is not enough to ensure data integrity.
Let’s take a look at a our sample User class.
class User validates :email, presence: true, uniqueness: true end
When you persist a user instance, Rails will validate your model by running a
SELECT query to see if any user records already exist with the provided email.
Assuming the record proves to be valid, Rails will run the
INSERT statement to
persist the user. This works great in development and may even work in
production if you’re running a single instance of a single process web server.
But you’re not running a lone instance of WEBrick, are you? No, to maximize requests per minute, you’re running Unicorn on multiple Heroku dynos, each with multiple web processes. Let’s take a look at what happens if just two of these processes are trying to create a user with the same email address at around the same time:
Uh oh. Now we’ve got a problem. We wanted the uniqueness validation to keep data consistent with our intentions, but it has failed. Why? Because we never told the database of our intentions.
Let’s make sure the database is in on the plan by telling it to create a unique
users.email. We do this with a unique index.
class AddEmailIndexToUser def change # If you already have non-unique index on email, you will need # to remove it before you're able to add the unique index. add_index :users, :email, unique: true end end
Alternatively, you can create the unique index when generating the migration or model with:
rails generate model user email:string:uniq
With the index in place, how does the above scenario play out now?
Now we have the database acting as our last line of defense in our war on
inconsistent data. The second
save operation will generate an
ActiveRecord::RecordNotUnique exception. In most cases, this will result in an
application error. If you need to provide a better experience, you can rescue
and handle that exception in the controller action or use
at the class level.
Your Rails application may also have several
relationships. Specifying a
has_one relationship merely sets the relationship
methods up to deal with a single object rather than a collection.
its own does nothing to ensure data integrity.
For example, we’ve decided to add a
Profile class to our application. Users
will have a single profile record. Our classes now look like this:
class User has_one :profile validates :email, presence: true, uniqueness: true end class Profile belongs_to :user end
We’d expect that the
profiles table would contain no duplicate
has_one doesn’t make any promises about that. We’ll have to add a
unique index to
user_id) to prevent data inconsistency.
You could search your project for
has_one and then cross reference that with a list of indexes pulled from your
database, or you could let a gem do that for you. Consistency
Fail is a gem that finds missing
unique indexes for you. Install and run it as detailed in the README.
One problem I’ve seen with
consistency_fail is that the
has_one searches do
not properly recognize polymorphic relationships. It will suggest a unique index
on the id column when what you really need is a compound index on the type and
Rails does many things, but data integrity validations are not one of them. Your relational database is designed to enforce data integrity; let it.