Exchanging values of two records with unique-validated fields

4 views
Skip to first unread message

Dmitry Suzdalev

unread,
Jan 5, 2012, 8:35:50 AM1/5/12
to rubyonra...@googlegroups.com
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 :)

Thanks.

Peter Vandenabeele

unread,
Jan 5, 2012, 9:09:49 AM1/5/12
to rubyonra...@googlegroups.com
If you mean "atomic" on the level of saving to the database
(all or nothing), you would need a database transaction. 

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 ...

2) 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 Vandenabeele
http://twitter.com/peter_v

Dmitry Suzdalev

unread,
Jan 5, 2012, 9:16:24 AM1/5/12
to rubyonra...@googlegroups.com
On 5 January 2012 18:09, Peter Vandenabeele <pe...@vandenabeele.com> wrote:
If you mean "atomic" on the level of saving to the database
(all or nothing), you would need a database transaction. 

Second, to circumvent the problem of the uniqueness validation,
I see 2 solutions:
<snip>

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!
Reply all
Reply to author
Forward
0 new messages