Firstly, can anyone explain how unique index works in databases?
Suppose I have a User model with a name column
and I add a unique index
on it but in the model (user.rb) I just have a presence
validator on the name
field.
So now when I'm trying to create two users with same name, I get PGError
duplicate key value violates unique constraint "index_users_on_name"
So It looks to me like the unique index
working same as uniqueness validator
(?)
If so then what about foreign keys?
Lets say I have Post
model with belongs_to :user
association with User has_many :posts
.
And a foreign key user_id
in the posts table
with unique index. Then multiple posts cannot have a same user_id
.
Can someone explain how unique index
works?
I'm on Rails 4 with Ruby 2.0.0.
Unique indexes are indexes that help maintain data integrity by ensuring that no rows of data in a table have identical key values. When you create a unique index for an existing table with data, values in the columns or expressions that comprise the index key are checked for uniqueness.
Non-unique indexes are not explicitly specified in PostgreSQL. An index that isn't unique simply has the option to have duplicate value, yet that fact isn't shown in description of database tables. For example, the author column could be indexed but have multiple occurrences of a given value within that column.
PostgreSQL automatically creates a unique index when a unique constraint or primary key is defined for a table. The index covers the columns that make up the primary key or unique constraint (a multicolumn index, if appropriate), and is the mechanism that enforces the constraint.
A unique index ensures the index key columns do not contain any duplicate values. A unique index may consist of one or many columns. If a unique index has one column, the values in this column will be unique. In case the unique index has multiple columns, the combination of values in these columns is unique.
Here are the difference between unique index and validates_uniqueness_of
This is a patch to enable ActiveRecord to identify db-generated errors for unique constraint violations. For example, it makes the following work without declaring a validates_uniqueness_of:
create_table "users" do |t|
t.string "email", null: false
end
add_index "users", ["email"], unique: true
class User < ActiveRecord::Base
end
User.create!(email: '[email protected]')
u = User.create(email: '[email protected]')
u.errors[:email]
=> "has already been taken"
The benefits are speed, ease of use, and completeness --
Speed
With this approach you don't need to do a db lookup to check for uniqueness when saving (which can sometimes be quite slow when the index is missed -- https://rails.lighthouseapp.com/projects/8994/tickets/2503-validate... ). If you really care about validating uniqueness you're going to have to use database constraints anyway so the database will validate uniqueness no matter what and this approach removes an extra query. Checking the index twice isn't a problem for the DB (it's cached the 2nd time around), but saving a DB round-trip from the application is a big win.
Ease of use
Given that you have to have db constraints for true uniqueness anyway, this approach will let everything just happen automatically once the db constraints are in place. You can still use validates_uniqueness_of if you want to.
Completeness
validates_uniqueness_of has always been a bit of a hack -- it can't handle race conditions properly and results in exceptions that must be handled using somewhat redundant error handling logic. (See "Concurrency and integrity" section in http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMe...)
validates_uniqueness_of is not sufficient to ensure the uniqueness of a value. The reason for this is that in production, multiple worker processes can cause race conditions:
Two concurrent requests try to create a user with the same name (and we want user names to be unique)
The requests are accepted on the server by two worker processes who will now process them in parallel
Both requests scan the users table and see that the name is available
Both requests pass validation and create a user with the seemingly available name
For more clear understanding please check this
If you create a unique index for a column it means you’re guaranteed the table won’t have more than one row with the same value for that column. Using only validates_uniqueness_of validation in your model isn’t enough to enforce uniqueness because there can be concurrent users trying to create the same data.
Imagine that two users tries to register an account with the same email where you have added validates_uniqueness_of :email in your user model. If they hit the “Sign up” button at the same time, Rails will look in the user table for that email and respond back that everything is fine and that it’s ok to save the record to the table. Rails will then save the two records to the user table with the same email and now you have a really shitty problem to deal with.
To avoid this you need to create a unique constraint at the database level as well:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :email
...
end
add_index :users, :email, unique: true
end
end
So by creating the index_users_on_email unique index you get two very nice benefits. Data integrity and good performance because unique indexes tends to be very fast.
If you put unique: true in your posts table for user_id then it will not allow to enter duplicate records with same user_id.
Db Unique index and i quote from this SO question is:
Unique Index in a database is an index on that column that also enforces the constraint that you cannot have two equal values in that column in two different rows
While ROR uniqueness validation should do the same but from application level, meaning that the following scenario could rarely but easily happen:
Which happened to me a month ago and got advise to solve it using DB unique index in this SO question
By the way this workaround is well documented in Rails:
The best way to work around this problem is to add a unique index to the database table using ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the rare case that a race condition occurs, the database will guarantee the field’s uniqueness
As for the uniqueness goes,
Uniqueness validates that the attribute's value is unique right before the object gets saved. It does not create a uniqueness constraint in the database, so it may happen that two different database connections create two records with the same value for a column that you intend to be unique. To avoid that, you must create a unique index on both columns in your database.
Also, if you just have validates_uniqueness_of
at model level then you would be restricted to insert duplicate records from rails side BUT not at the database level.
SQL inject queries through dbconsole would insert duplicate records without any problem.
When you say that you created a foreign key with index on "user_id" in "posts" table then by default rails only creates an index
on it and NOT a unique index
. If you have 1-M relationship then there is no point in unique index in your case.
If you had unique: true
in your posts table for "user_id"
then there is no way that duplicate records with same "user_id" would go through
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With