0

I've seen other SO questions like - How do you validate uniqueness of a pair of ids in Ruby on Rails? - which describes adding a scoped parameter to enforce uniqueness of a key pair, i.e. (from the answer)

validates_uniqueness_of :user_id, :scope => [:question_id]

My question is how do you do this kind of validation for an entire row of data?

In my case, I have five columns and the data should only be rejected if all five are the same. This data is not user entered and the table is essentially a join table (no id or timestamps).

My current thought is to search for a record with all of the column values and only create if the query returns nil but this seems like a bad work around. Is there an easier 'rails way' to do this?

Community
  • 1
  • 1
jkeuhlen
  • 4,401
  • 23
  • 36
  • `validates_uniqueness_of :user_id, :scope => [*self.column_names]` might work (but it includes `created_at`, `id` and `updated_at`, which should be removed) – MrYoshiji Jul 21 '14 at 19:07
  • Should have added that my table is basically a beefed up join table so no id or timestamps. – jkeuhlen Jul 21 '14 at 19:27

2 Answers2

2

You'll need to create a custom validator (http://guides.rubyonrails.org/active_record_validations.html#performing-custom-validations):

class TotallyUniqueValidator < ActiveModel::Validator
  def validate(record)
    if record.attributes_for_uniqueness.values.uniq.size == 1
      record.errors[:base] << 'All fields must be unique!'
    end
  end
end

class User
  validates_with TotallyUniqueValidator

  def attributes_for_uniqueness
    attributes.except :created_at, :updated_at, :id
  end
end

The important line here is:

if record.attributes_for_uniqueness.values.uniq.size == 1

This will grab a hash of all the attributes you want to check for uniqueness (in this case everything except id and timestamps) and converts it to an array of just the values, then calls uniq on it which returns only uniq values and if the size is 1 then they were all the same value.

Update based on your comment that your table doesn't have an id or timestamps:

You can then simply do:

if record.attributes.except(:id).values.uniq.size == 1

...because I'm pretty sure it still has an id unless you're sure it doesn't then just remove the except part.

DiegoSalazar
  • 13,361
  • 2
  • 38
  • 55
  • Do you know of any way to do this closer to the database level? I saw some answers on using indexes to create unique values as well and might prefer that over a model validator because my data is not user entered. – jkeuhlen Jul 21 '14 at 19:25
  • I doubt there is an easy way to do this on the database level. Database indexes work vertically by the column not horizontally on the row. A unique index on column1 will be unique across that column only and won't care that you specified a unique index on column2 and so on. Barring that you'd have to write a TRIGGER that does the checking: http://www.postgresql.org/docs/9.2/static/plpgsql-trigger.html – DiegoSalazar Jul 21 '14 at 19:31
  • Just FYI most join (or join-like) tables don't have ID columns because they are created with `create_table "table_name", id: false` so I'm definitely sure it doesn't have an ID column – jkeuhlen Jul 22 '14 at 13:52
  • If I have a simple has_many and belongs_to relationship will this also use the foreign key in the validations? I have many child-records that should be unique in relation to each parent-object they belong_to, but may otherwise may turn up multiple times if you compare different objects – Huw Feb 19 '15 at 17:28
1

You can add a unique index to the table in a migration:

add_index :widgets, [:column1, :column2, :column3, :column4, :column5], unique: true

The resulting index will require that each combination of the 5 columns must be unique.

infused
  • 24,000
  • 13
  • 68
  • 78