swingfuture
swingfuture

Reputation: 1108

Rails custom validation checking for duplicates

A table has following fields: badge_id, output_id, timely, removed, updated_at. For each badge_id, there can't have two valid records with the same output_id. But it doesn't mean that (badge_id, output_id) is a unique combination. Removed column indicates the current row has been removed or not. Basically delete or update operation triggers inserting a new row in the table with the latest change. So for example, we have a record like this:

badge_id| output_id| removed| timely | updated_at
1       | 1        | N      | Y      | 2013-11-26

To remove that record, we actually insert another row and now it reads like

badge_id| output_id| removed| timely | updated_at
1       | 1        | N      | Y      | 2013-11-26
1       | 1        | Y      | Y      | 2013-11-27

Because the latest record of (badge_id: 1, output_id: 1) has removed column set, it means that combination has been deleted. But I can't have two rows of same (badge_id: 1, output_id: 1), both have removed as "N" like:

badge_id| output_id| removed| timely | updated_at
1       | 1        | N      | N      | 2013-11-26
1       | 1        | N      | Y      | 2013-11-27

So every time to add a new output_id for a certain badge_id, I have to check for duplication. But usual validates uniqueness of (badge_id, output_id) from ActiveModel doesn't work here. How do I write a clean custom validation for this? Thanks.

UPDATE:

I think I might have missed some key points. A record can be added and then deleted and then added repeatedly. So a combination of (badge_id, output_id, removed) isn't unique either. When add a new record, we need to check for (badge_id, output_id), whether latest record has removed set as 'Y' or not.

So for possible answer like

validate_uniqueness_of :badge_id, scope: [:output_id], 
conditions: -> { where(removed: "N") }

At the condition where clause, it should have order by updated_at desc and the first one has removed: 'N'. How do I fit that kind of condition into this one line code? Or there's a better way of doing this?

Upvotes: 0

Views: 871

Answers (3)

DiegoSalazar
DiegoSalazar

Reputation: 13541

Hopefully I understand your use case properly.

Try validating the uniqueness of the removed and scoping it to both the badge_id and output_id columns but only when the removed field is N:

class Model < ActiveRecord::Base
  validates_uniqueness_of :removed, 
    scope: [:badge_id, :output_id], 
    conditions: -> { where.not(removed: 'Y') }
end

There's a chance it might just work.

Upvotes: 0

CDub
CDub

Reputation: 13354

You could do something like:

validates :unique_badge_and_output_ids

Then unique_badge_and_output_ids could be:

def unique_badge_and_output_ids
  unless Object.find_by_badge_id_and_output_id_and_removed(self.badge_id, self.output_id, self.removed).blank?
    self.errors.add "record already exists" # obviously a better error here would be ideal
  end
end

Upvotes: 2

Farley Knight
Farley Knight

Reputation: 1793

You can specify an SQL condition on validates_uniqueness_of:

http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of

It is also possible to limit the uniqueness constraint to a set of records matching certain conditions. In this example archived articles are not being taken into consideration when validating uniqueness of the title attribute:

class Article < ActiveRecord::Base
  validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') } 
end

So in your example:

class YourModel < AR::Base
  validate_uniqueness_of :badge_id, scope: [:output_id], 
    conditions: -> { where(removed: "N") }
end

Upvotes: 0

Related Questions