168

What I need is a migration to apply unique constraint to a combination of columns. i.e. for a people table, a combination of first_name, last_Name and Dob should be unique.

Arslan Ali
  • 17,418
  • 8
  • 58
  • 76
rangalo
  • 5,448
  • 8
  • 46
  • 66
  • 3
    Just an FYI for anyone reading this question: Don't make a constraint based on first name, last name, and date of birth, because more than one person can match on all three of those. (This is mentioned in an answer, but it isn't really an answer.) – cesoid Nov 25 '20 at 13:49

7 Answers7

284

add_index :people, [:firstname, :lastname, :dob], unique: true

Artur INTECH
  • 6,024
  • 2
  • 37
  • 34
Robert Speicher
  • 15,382
  • 6
  • 40
  • 45
  • 15
    I think that's adding a unique index, *not* a constraint. Or does the index add the constraint as well? – Paul Cantrell Aug 24 '11 at 16:24
  • 20
    Nope, it's all good. My bad! The unique constraint comes with the unique index. – Paul Cantrell Aug 24 '11 at 16:34
  • 8
    I agree with @paul-cantrell : ain't there any way to only add a constraint, not an index (which has db storage influences) – Augustin Riedinger Jul 04 '13 at 15:24
  • 1
    @AugustinRiedinger, little reason to put constraint in DB then. Instead, use a validation on the model: http://stackoverflow.com/questions/4870961/rails-validate-uniqueness-of-multiple-columns – Ari Apr 25 '14 at 17:04
  • 22
    The problem with the model level validation is that it doesn't scale. Two servers could run the same data through at the same time (like a double tap on an api heavy app) I have two identical records in my DB right now and the model has the validation.. – baash05 Mar 17 '15 at 01:26
  • 7
    I like to have both.. Just to make sure – baash05 Mar 17 '15 at 01:26
  • 2
    @baash05 I'm here because of this exactly problem, two servers running the same data at the same time, requested by my API. The model validation is useless in this case –  Jun 26 '15 at 12:17
  • For lots of discussion about why you need the database to enforce this, not just the model, see http://www.bailis.org/papers/feral-sigmod2015.pdf – Nathan Long Dec 28 '16 at 22:00
  • 1
    @PaulCantrell I got confused by your comments; the first comment is about whether an index creates a constraint as well, whereas the second comment asserts that a constraint creates an index as well ... while the latter statement is true (for postgres), the converse isn't and requires a different approach, like [this answer](https://stackoverflow.com/a/28704445/1338292) – Ja͢ck Jan 12 '18 at 01:10
  • No, both comments were about whether a unique index automatically creates a unique constraint. I was indeed only considering Postgres. – Paul Cantrell Jan 12 '18 at 01:25
  • Would be helpful to instruct whether or not this operation is compatible with migration "change". – karns Sep 03 '19 at 15:07
28

You may want to add a constraint without an index. This will depend on what database you're using. Below is sample migration code for Postgres. (tracking_number, carrier) is a list of the columns you want to use for the constraint.

class AddUniqeConstraintToShipments < ActiveRecord::Migration
  def up
    execute <<-SQL
      alter table shipments
        add constraint shipment_tracking_number unique (tracking_number, carrier);
    SQL
  end

  def down
    execute <<-SQL
      alter table shipments
        drop constraint if exists shipment_tracking_number;
    SQL
  end
end

There are different constraints you can add. Read the docs

Josh
  • 8,329
  • 4
  • 36
  • 33
  • 14
    [Docs for PostgreSQL 9.4](http://www.postgresql.org/docs/9.4/static/ddl-constraints.html#DDL-CONSTRAINTS-UNIQUE-CONSTRAINTS) say: _Adding a unique constraint will automatically create a unique btree index on the column or group of columns used in the constraint. A uniqueness constraint on only some rows can be enforced by creating a partial index._ So IMHO there's no need to drop to raw SQL when the outcome will be basically the same as using the `add_index` method. ;) – Rafał Cieślak Mar 23 '15 at 10:32
  • 8
    Actually there is one reason: It is an implementation detail and discouraged by the [docs](http://www.postgresql.org/docs/9.3/static/indexes-unique.html). Also note that you cannot refer to the constraint by name, since it is not added to the `pg_constraint` table. – kaikuchn May 11 '15 at 10:58
28

According to howmanyofme.com, "There are 46,427 people named John Smith" in the United States alone. That's about 127 years of days. As this is well over the average lifespan of a human being, this means that a DOB clash is mathematically certain.

All I'm saying is that that particular combination of unique fields could lead to extreme user/customer frustration in future.

Consider something that's actually unique, like a national identification number, if appropriate.

(I realise I'm very late to the party with this one, but it could help future readers.)

A Fader Darkly
  • 3,516
  • 1
  • 22
  • 28
20

For completeness sake, and to avoid confusion here are 3 ways of doing the same thing:
Adding a named unique constraint to a combination of columns in Rails 5.2+

Let's say we have Locations table that belongs to an advertiser and has column reference_code and you only want 1 reference code per advertiser. so you want to add a unique constraint to a combination of columns and name it.

Do:

rails g migration AddUniquenessConstraintToLocations

And make your migration look either something like this one liner:

class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
  def change
    add_index :locations, [:reference_code, :advertiser_id], unique: true, name: 'uniq_reference_code_per_advertiser'
  end
end

OR this block version.

class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
  def change
    change_table :locations do |t|
     t.index ['reference_code', 'advertiser_id'], name: 'uniq_reference_code_per_advertiser', unique: true
    end
  end
end

OR this raw SQL version

class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
  def change
      execute <<-SQL
          ALTER TABLE locations
            ADD CONSTRAINT uniq_reference_code_per_advertiser UNIQUE (reference_code, advertiser_id);
        SQL
  end
end

Any of these will have the same result, check your schema.rb

Khalil Gharbaoui
  • 6,557
  • 2
  • 19
  • 26
11

Hi You may add unique index in your migration to the columns for example

add_index(:accounts, [:branch_id, :party_id], :unique => true)

or separate unique indexes for each column

Bohdan
  • 8,298
  • 6
  • 41
  • 51
  • Sorry, it worked, first I tried by editing and existing migration which didn't work, then added a new one and it worked, thanks. – rangalo Jul 30 '10 at 11:10
5

In the typical example of a join table between users and posts:

create_table :users
create_table :posts

create_table :ownerships do |t|
  t.belongs_to :user, foreign_key: true, null: false
  t.belongs_to :post, foreign_key: true, null: false
end

add_index :ownerships, [:user_id, :post_id], unique: true

Trying to create two similar records will throw a database error (Postgres in my case):

ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR:  duplicate key value violates unique constraint "index_ownerships_on_user_id_and_post_id"
DETAIL:  Key (user_id, post_id)=(1, 1) already exists.
: INSERT INTO "ownerships" ("user_id", "post_id") VALUES ($1, $2) RETURNING "id"

e.g. doing that:

Ownership.create!(user_id: user_id, post_id: post_id)
Ownership.create!(user_id: user_id, post_id: post_id)

Fully runnable example: https://gist.github.com/Dorian/9d641ca78dad8eb64736173614d97ced

db/schema.rb generated: https://gist.github.com/Dorian/a8449287fa62b88463f48da986c1744a

Dorian
  • 22,759
  • 8
  • 120
  • 116
0

If you are creating a new table just add unique: true

class CreatePosts < ActiveRecord::Migration[6.0]
  def change
    create_table :posts do |t|
      t.string :title, unique: true
      t.text :body
      t.references :user, foreign_key: true
      t.timestamps
    end

    add_index :posts, :user_id, unique: true
  end
end