Humane database errors in ActiveRecord
ActiveRecord doesn't really play well with database constraints and other builtin data consistency mechanisms.
It comes with its own mechanisms for ensuring consistency, but it can be coerced into using the mechanisms from the database as well.
Ensuring unique names in a users table
Imagine you've implemented an
ActiveRecord model named
User, with the following schema:
create_table "users", force: true do |t| t.string "name" end
Names need to be unique across users, so you've also added a unique constrain on the
add_index "users", ["name"], unique: true
This way we know we'll never get two rows with the same name value in the database:
>> User.create!(:name => "Bob") => #<User id: 1, name: "Bob"> >> User.create!(:name => "Bob") ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_users_on_name" DETAIL: Key (name)=(Bob) already exists. : INSERT INTO "users" ("name") VALUES ($1) RETURNING "id"
Not the cleanest of results but pretty much as expected: We hit the unique constraint, the database refuses to store the data, and ActiveRecord raises an exception.
If this were to happen in a running Rails app, the end user would get a HTTP 500 error page, which is not the most user friendly of things.
One way to work around this is to add a uniqueness validation of the
validates :name, :uniqueness => true. This is fine, gives the users readable (and localizable) error messages and work in by far the majority of cases.
If you can, by all means use the validation.
Rescuing database errors
But how do we handle those cases where the validation won't suffice and we don't want to show HTTP 500 error pages to our users?
If we create our
save method, we can actually catch the database error before it bubbles up to the user interface and - hopefully - do something better with it:
class User < ActiveRecord::Base def save(*args) super rescue ActiveRecord::RecordNotUnique => error errors[:base] << error.message false end end
Now, at least the user gets a clue as to what has happened, but perhaps not in the most user friendly fashion:
You could naturally place any other message into the errors object if you so desire - for example one that makes more sense to normal people.