Exchanging values of two records with unique-validated fields

Hi fellows!

Is there a way to atomically exchange values of records which are validated using ‘validates_uniqueness_of’?

Some details follow.

Imagine I have a model:

class Item < ActiveRecord::Base

validates_uniqueness_of :weight

end

‘weight’ is a sorting weight.

I have an index.html view which has a list of “Item” records wrapped in jQueryUI’s sortable container.

My goal is to do some ajax request (POST i guess) whenever user changes items’ order. I.e. i plan to do end up with controller action that calls function like:

class Item

def self.exchange_weights(id1, id2)

item1 = Item.find(id1)

item2 = Item.find(id2)

weight1 = Item.find(id1).weight

weight2 = Item.find(id2).weight

item1.weight = some_temp_weight

item2.weight = weight1

item1.weight = weight2

end

end

Is this the right way to do this?

Or can I somehow just do a separate POST requests on these Item’s instances by triggering ItemsController#update for each of them and passing weight - but here validations would not allow me to do this unless I miss something - that’s why I desided to ask :slight_smile:

Thanks.

If you mean “atomic” on the level of saving to the database

(all or nothing), you would need a database transaction.

http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html

Second, to circumvent the problem of the uniqueness validation,

I see 2 solutions:

  1. what you propose above:

UNTESTED:

def self.exchange_weights(id1, id2)

item1 = Item.find(id1)

item2 = Item.find(id2)

weight1 = item1.weight

weight2 = item2.weight

Item.transaction do

item1.weight = some_temp_weight

item1.save!

item2.weight = weight1

item2.save!

item1.weight = weight2

item1.save!

end # you still need to catch the exception here

end

This could fail … e.g. if 2 parallel processed each write the same

some_temp_weight exactly at the same time …

  1. Alternative:

Make the validation dependent on an instance variable

UNTESTED

class Item

attr_accessor :no_weight_uniqueness_validation

validates :weight, :uniqueness => true, :unless => @no_weight_uniqueness_validation

end

def self.exchange_weights(id1, id2)

item1 = Item.find(id1)

item2 = Item.find(id2)

weight1 = item1.weight

weight2 = item2.weight

Item.transaction do

item2.weight = weight1

item2.no_weight_uniqueness_validation = true

item2.save!

item1.weight = weight2

item1.save!

end # you still need to catch the exception here

end

This will NOT work if you also have the uniqueness validation

in the database itself … which you should, since the

uniqueness validation in Rails is not automatically protected

against race conditions when multiple parallell processes

write to the same database.

HTH,

Peter

Oh, it’s clearer now. And, yes I have just saw in rails’ apidocs that I should do add_index on the DB level too - to prevent possible clashes. Didn’t know that until now, thanks.

I guess that I will start with the first solution then. And maybe will improve it to be the latter if find the need too.

Thanks, Peter!