pdoherty926
pdoherty926

Reputation: 10399

Prevent duplicate has_many records in Rails 5

Given the following models:

class Client < ApplicationRecord
  has_many :preferences
  validates_associated :preferences
  accepts_nested_attributes_for :preferences
end

class Preference < ApplicationRecord
  belongs_to :client
  validates_uniqueness_of :block, scope: [:day, :client_id]
end

I'm still able to create preferences with duplicate days* when creating a batch of preferences during client creation. This is (seemingly) because the client_id foreign key isn't available when the validates_uniqueness_of validation is run. (*I have an index in place which prevents the duplicate from being saved, but I'd like to catch the error, and return a user friendly error message, before it hits the database.)

Is there any way to prevent this from happening via ActiveRecord validations?

EDIT: This appears to be a known issue.

Upvotes: 1

Views: 602

Answers (1)

AndrewSwerlick
AndrewSwerlick

Reputation: 913

There's not a super clean way to do this with AR validations when you're batch inserting, but you can do it manually with the following steps.

  1. Make a single query to the database using a Postgresql VALUES list to load any potentially duplicate records.
  2. Compare the records you are about to batch create and pull out any duplicates
  3. Manually generate and return your error message

Step 1 looks a little like this

# Build array of uniq attribute pairs we want to check for
uniq_attrs = new_collection.map do |record|
  [
    record.day,
    record.client_id,
  ]
end

# santize the values and create a tuple like ('Monday', 5)
values = uniq_attrs.map do |attrs|
  safe = attrs.map {|v| ActiveRecord::Base.connection.quote(v)}
  "( #{safe.join(",")} )"
end

existing = Preference.where(%{
    (day, client_id) in
    (#{values.join(",")})
 })

# SQL Looks like 
# select * from preferences where (day, client_id) in (('Monday',5), ('Tuesday', 3) ....)

Then you can take the collection existing and use it in steps 2 and 3 to pull out your duplicates and generate your error messages.

When I've needed this functionality, I've generally made it a self method off my class, so something like

class Preference < ApplicationRecord

  def self.filter_duplicates(collection)
    # blah blah blah from above

    non_duplicates = collection.reject do |record|
      existing.find do |exist|
        exist.duplicate?(record)
      end
    end

    [non_duplicates, existing]

  end

  def duplicate?(record)
    record.day == self.day && 
    record.client_id = self.client_id
  end
end

Upvotes: 1

Related Questions