I discovered an interesting aspect of has_many behavior that I''m struggling to work around. I''m not sure if I''m doing something wrong, or if it''s a legitimate bug, or if it''s an inherent part of Rails that I just have to learn to deal with. It boils down to these two problems: - changes in collection objects (i.e. models that belong_to a container model) don''t propagate to the container object''s collection view until the collection objects are saved and the association reloaded - the collection= method doesn''t do anything when you give it updated objects I encountered these problems when I was updating both the container object and some or all of the collection objects at the same time, and I wanted to use the container.collection before everything is saved (i.e. for collective validation). The first problem comes from the fact that container.collection.find(id) returns a completely different Ruby object than the objects contained in the container.collection array. When you modify the object returned by find, those changes don''t show up in the container.collection array until the modified objects are saved and reloaded from the database. How could these be better synced up? I tried using container.collection= to overwrite the ruby objects in the collection with those that were generated by find, but that doesn''t work, because of the second problem. The collection= method ignores objects that match the id of an object already in the collection! So even if the objects you pass in to container.collection= are completely different except for their ids, the collection remains unchanged. So that''s a weird behavior, too. Why is it like that? Here''s my simplified test case, part of a new Web 2.0 app, Rain''d. :-) The umbrellas are all named, and we want to make sure that each person has uniquely-named umbrellas. class Person < ActiveRecord::Base has_many :umbrellas def validate errors.add("umbrellas", "must be uniquely named") unless no_duplicate_names? end def no_duplicate_names? names = umbrellas.collect {|u| u.name} names.length == names.uniq.length end end class Umbrella < ActiveRecord::Base belongs_to :person end In people_controller.rb (note the comment in the middle): def update @person = Person.find(params[:person][:id], :include=>:umbrellas) @person.attributes = params[:person] @umbrellas = params[:umbrellas].values.collect do |u| umbrella = u[:id] ? @person.umbrellas.find(u[:id]): @person.umbrellas.build(u) umbrella.attributes = u umbrella end begin Person.transaction do @umbrellas.each {|u| u.save!} # in order for the validation to work, you have to save the umbrellas first, # then call this reloading trick here: @person.umbrellas(true) @person.save! # now this will validate end flash[:notice] = ''Person was successfully updated.'' redirect_to :action => ''show'', :id=>@person.id rescue => exception flash[:notice] = exception.to_s render_scaffold(''edit'') end end Here''s a transcript showing the second problem, from a breakpoint I set in the update method. It illustrates some of the weirdness that''s going on here.> @umbrellas = @person.umbrellas=> [#<Umbrella:0x38d7218 @attributes={"name"=>"U. Horatio", "id"=>"1", "person_i d"=>"1"}>, #<Umbrella:0x38d5ca0 @attributes={"name"=>"U. Glavellus", "id"=>"2", "person_id"=>"1"}>]> @umbrellas.each{|u| u.name = u.id.to_s}=> [#<Umbrella:0x38d7218 @attributes={"name"=>"1", "id"=>"1", "person_id"=>"1"}> , #<Umbrella:0x38d5ca0 @attributes={"name"=>"2", "id"=>"2", "person_id"=>"1"}>]> @person.umbrellas=> [#<Umbrella:0x38d7218 @attributes={"name"=>"1", "id"=>"1", "person_id"=>"1"}> , #<Umbrella:0x38d5ca0 @attributes={"name"=>"2", "id"=>"2", "person_id"=>"1"}>]> @umbrellas = [@person.umbrellas.find(1), @person.umbrellas.find(2)]=> [#<Umbrella:0x389b578 @attributes={"name"=>"U. Horatio", "id"=>"1", "person_i d"=>"1"}>, #<Umbrella:0x38977b0 @attributes={"name"=>"U. Glavellus", "id"=>"2", "person_id"=>"1"}>]> @person.umbrellas=> [#<Umbrella:0x38d7218 @attributes={"name"=>"1", "id"=>"1", "person_id"=>"1"}> , #<Umbrella:0x38d5ca0 @attributes={"name"=>"2", "id"=>"2", "person_id"=>"1"}>]> @person.umbrellas = @umbrellas=> [#<Umbrella:0x389b578 @attributes={"name"=>"U. Horatio", "id"=>"1", "person_i d"=>"1"}>, #<Umbrella:0x38977b0 @attributes={"name"=>"U. Glavellus", "id"=>"2", "person_id"=>"1"}>]> @person.umbrellas=> [#<Umbrella:0x38d7218 @attributes={"name"=>"1", "id"=>"1", "person_id"=>"1"}> , #<Umbrella:0x38d5ca0 @attributes={"name"=>"2", "id"=>"2", "person_id"=>"1"}>] Sorry for all the text, and thanks if you''ve gotten this far. It seems like this is a relatively simple thing to want to accomplish, and I''m surprised that in doing so I''ve run into a bunch of Rails weirdnesses. I can work around them fine, I''m just wondering if there''s a Better Way(tm). -RYaN
So one way of getting around the first problem is to return the actual object from within the collection. @umbrellas = params[:umbrellas].values.collect do |u| umbrella = u[:id] ? @person.umbrellas.detect{|u| u.id =u[:id].to_i}: @person.umbrellas.build(u) umbrella.attributes = u umbrella end This means that you don''t have to reload the umbrellas collection in the transaction anymore. However, this solution seems inelegant, and slow when you have a lot of umbrellas. Is there no better way? -RYaN On 5/26/06, Ryan Williams <mr.cruft@gmail.com> wrote:> I discovered an interesting aspect of has_many behavior that I''m > struggling to work around. I''m not sure if I''m doing something wrong, > or if it''s a legitimate bug, or if it''s an inherent part of Rails that > I just have to learn to deal with. > > It boils down to these two problems: > - changes in collection objects (i.e. models that belong_to a > container model) don''t propagate to the container object''s collection > view until the collection objects are saved and the association > reloaded > - the collection= method doesn''t do anything when you give it updated objects > > I encountered these problems when I was updating both the container > object and some or all of the collection objects at the same time, and > I wanted to use the container.collection before everything is saved > (i.e. for collective validation). > > The first problem comes from the fact that > container.collection.find(id) returns a completely different Ruby > object than the objects contained in the container.collection array. > When you modify the object returned by find, those changes don''t show > up in the container.collection array until the modified objects are > saved and reloaded from the database. How could these be better > synced up? > > I tried using container.collection= to overwrite the ruby objects in > the collection with those that were generated by find, but that > doesn''t work, because of the second problem. The collection= method > ignores objects that match the id of an object already in the > collection! So even if the objects you pass in to > container.collection= are completely different except for their ids, > the collection remains unchanged. So that''s a weird behavior, too. > Why is it like that? > > Here''s my simplified test case, part of a new Web 2.0 app, Rain''d. > :-) The umbrellas are all named, and we want to make sure that each > person has uniquely-named umbrellas. > > class Person < ActiveRecord::Base > has_many :umbrellas > > def validate > errors.add("umbrellas", "must be uniquely named") unless > no_duplicate_names? > end > > def no_duplicate_names? > names = umbrellas.collect {|u| u.name} > names.length == names.uniq.length > end > end > > class Umbrella < ActiveRecord::Base > belongs_to :person > end > > In people_controller.rb (note the comment in the middle): > > def update > @person = Person.find(params[:person][:id], :include=>:umbrellas) > @person.attributes = params[:person] > @umbrellas = params[:umbrellas].values.collect do |u| > umbrella = u[:id] ? @person.umbrellas.find(u[:id]): > @person.umbrellas.build(u) > umbrella.attributes = u > umbrella > end > begin > Person.transaction do > @umbrellas.each {|u| u.save!} > > # in order for the validation to work, you have to save the > umbrellas first, > # then call this reloading trick here: > @person.umbrellas(true) > > @person.save! # now this will validate > end > flash[:notice] = ''Person was successfully updated.'' > redirect_to :action => ''show'', :id=>@person.id > rescue => exception > flash[:notice] = exception.to_s > render_scaffold(''edit'') > end > end > > > Here''s a transcript showing the second problem, from a breakpoint I > set in the update method. It illustrates some of the weirdness that''s > going on here. > > > @umbrellas = @person.umbrellas > => [#<Umbrella:0x38d7218 @attributes={"name"=>"U. Horatio", "id"=>"1", "person_i > d"=>"1"}>, #<Umbrella:0x38d5ca0 @attributes={"name"=>"U. Glavellus", "id"=>"2", > "person_id"=>"1"}>] > > > @umbrellas.each{|u| u.name = u.id.to_s} > => [#<Umbrella:0x38d7218 @attributes={"name"=>"1", "id"=>"1", "person_id"=>"1"}> > , #<Umbrella:0x38d5ca0 @attributes={"name"=>"2", "id"=>"2", "person_id"=>"1"}>] > > > @person.umbrellas > => [#<Umbrella:0x38d7218 @attributes={"name"=>"1", "id"=>"1", "person_id"=>"1"}> > , #<Umbrella:0x38d5ca0 @attributes={"name"=>"2", "id"=>"2", "person_id"=>"1"}>] > > > @umbrellas = [@person.umbrellas.find(1), @person.umbrellas.find(2)] > => [#<Umbrella:0x389b578 @attributes={"name"=>"U. Horatio", "id"=>"1", "person_i > d"=>"1"}>, #<Umbrella:0x38977b0 @attributes={"name"=>"U. Glavellus", "id"=>"2", > "person_id"=>"1"}>] > > > @person.umbrellas > => [#<Umbrella:0x38d7218 @attributes={"name"=>"1", "id"=>"1", "person_id"=>"1"}> > , #<Umbrella:0x38d5ca0 @attributes={"name"=>"2", "id"=>"2", "person_id"=>"1"}>] > > > @person.umbrellas = @umbrellas > => [#<Umbrella:0x389b578 @attributes={"name"=>"U. Horatio", "id"=>"1", "person_i > d"=>"1"}>, #<Umbrella:0x38977b0 @attributes={"name"=>"U. Glavellus", "id"=>"2", > "person_id"=>"1"}>] > > > @person.umbrellas > => [#<Umbrella:0x38d7218 @attributes={"name"=>"1", "id"=>"1", "person_id"=>"1"}> > , #<Umbrella:0x38d5ca0 @attributes={"name"=>"2", "id"=>"2", "person_id"=>"1"}>] > > Sorry for all the text, and thanks if you''ve gotten this far. It > seems like this is a relatively simple thing to want to accomplish, > and I''m surprised that in doing so I''ve run into a bunch of Rails > weirdnesses. I can work around them fine, I''m just wondering if > there''s a Better Way(tm). > > -RYaN >
Hello Ryan, The no_duplicate_names? method pulls umbrellas from the database with this line: names = umbrellas.collect {|u| u.name} Try switching that line to use the instance variable @umbrellas such that: names = @umbrellas.collect {|u| u.name}
Huh, I didn''t think of that. But it doesn''t seem to make a difference. I think that the umbrellas method uses the @umbrellas instance variable if it exists, and otherwise creates it from the database. So in my case, the @umbrellas array is out-of-sync. -RYaN On 5/27/06, travis michel <meshac.ruby@gmail.com> wrote:> Hello Ryan, > > The no_duplicate_names? method pulls umbrellas from the database with this line: > names = umbrellas.collect {|u| u.name} > > Try switching that line to use the instance variable @umbrellas such that: > names = @umbrellas.collect {|u| u.name} > > _______________________________________________ > Rails mailing list > Rails@lists.rubyonrails.org > http://lists.rubyonrails.org/mailman/listinfo/rails > > >