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.

On Thu, Jan 5, 2012 at 2:35 PM, Dmitry S. [email protected]
wrote:

end
def self.exchange_weights(id1, id2)
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:

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


Peter V.
http://twitter.com/peter_v
http://rails.vandenabeele.com

On 5 January 2012 18:09, Peter V. [email protected]
wrote:

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:

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!