So you forgot to add a uniqueness constraint to a table in your db and over time, race conditions have wreaked havoc on your validates_uniqueness_of validations.

Time to clean out those duplicates!

module DuplicateFinder
 
  def find_duplicate_sets_on_keys *uniqueness_keys
    uniqueness_list = uniqueness_keys.join(', ')
    find(:all, :select => "#{uniqueness_list}, COUNT(#{primay_key}) AS total", :group => "#{uniqueness_list} HAVING total > 1")
  end
 
  def find_duplicates_for_set duplicate_set, options = {}
    uniqueness_keys = duplicate_set.attributes.reject do |attr_name, attr_value| attr_name == 'total' end.keys
    condition_values = uniqueness_keys.map do |uniqueness_key| duplicate_set.send uniqueness_key end
    uniqueness_condition = [ uniqueness_keys.map do |uniqueness_key| "#{uniqueness_key} = ?" end.join(' AND ') ] + condition_values
 
    with_scope :find => { :conditions => uniqueness_condition } do
      find(:all, options)
    end
  end
 
end
 
class ActiveRecord::Base
 
  class << self
    include DuplicateFinder
  end
 
end

Now you find and operate on duplicates within your migration like so:

require 'duplicate_finder'
 
class AddIndexForFavoriteColor < ActiveRecord::Migration
  class FavoriteColor < ActiveRecord::Base
    def self.destroy_duplicates!
      find_duplicate_sets_on_keys(:user_id, :color_id).each do |duplicate_set|
        dups = find_duplicates_for_set(duplicate_set)
 
        # All but the last
        dups[0..-2].each do |duplicate|
          duplicate.destroy
        end
 
      end
    end
  end
 
  def self.up
    FavoriteColor.destroy_duplicates!
    add_index :favorite_colors, [:user_id, :color_id], :unique => true
  end
 
  def self.down
    remove_index :favorite_colors, :column => [:user_id, :color_id]
  end
end

I split the find_duplicate_sets_on_keys and find_duplicates_for_set up to minimize the number of records instantiated at any given time.

Leave a Comment

Enclose code in <code lang="ruby"></code> if you care.
Preview your comment using the button below.