Forum: Ruby on Rails Breakdowns in has_many abstraction

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
9a90e70ae03162543869ca68729de9c1?d=identicon&s=25 Ryan Williams (Guest)
on 2006-05-26 19:08
(Received via mailing list)
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
9a90e70ae03162543869ca68729de9c1?d=identicon&s=25 Ryan Williams (Guest)
on 2006-05-27 20:27
(Received via mailing list)
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
1c357f90ccd53e1d0e12e9dd08a4940c?d=identicon&s=25 travis michel (Guest)
on 2006-05-28 00:45
(Received via mailing list)
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}
9a90e70ae03162543869ca68729de9c1?d=identicon&s=25 Ryan Williams (Guest)
on 2006-05-28 01:40
(Received via mailing list)
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
This topic is locked and can not be replied to.