Strange behaviour with ActiveRecord has_many collections, Rails 4 compared to Rails 3. #12597

Closed
nwillia2 opened this Issue Oct 21, 2013 · 7 comments

Projects

None yet

4 participants

@nwillia2

Hi All,

I hope this is the correct place to post something like this. For the last year or so I have primarily been developing in the latest version of rails 3.0.x with Ruby 1.8.7. Lately, a new project has started in which we are using Ruby 2.0 and Rails 4.0.0. It is in this version of Rails which we seem to be getting a strange behaviour with has_many relationships.

The strange behaviour is related to the association.new or association.build methods.

Example:
Let's make two example models, Stories and Tasks...

class Story < ActiveRecord::Base
   has_many :tasks
end

class Task < ActiveRecord::Base
  belongs_to :story
end

In the 'new' action for tasks_controller, we would have the following code:

def new
  @story = Story.find(params[:story_id])
  @tasks = @story.tasks
  @task = @story.tasks.new
end 

This is how I have been doing things for the last year or so in Rails 3.0.x, with no issues whatsoever. As it's certainly much nicer than:

  @task = Task.new(:story_id => @story.id)

The issue is, that when doing the first example in Rails 4, @tasks would have an extra record inside it, which is the object stored in @task.

For example:

@tasks = @story.tasks
#<ActiveRecord::Associations::CollectionProxy 
  [#<Task id: 1, story_id: 1, description: "Some task 1">, 
   #<Task id: 2, story_id: 1, description: "Some task 2">, 
   #<Task id: 3, story_id: 1, description: "Some task 3">]>
@task = @story.tasks.new
#<Task id: nil, story_id: 1, description: nil>

Great, that's what I wanted... but, now when we check @tasks again:

#<ActiveRecord::Associations::CollectionProxy 
  [#<Task id: 1, story_id: 1, description: "Some task 1">, 
   #<Task id: 2, story_id: 1, description: "Some task 2">, 
   #<Task id: 3, story_id: 1, description: "Some task 3">, 
   #<Task id: nil, story_id: 1, description: nil>]>

No!! This is not what happens in Rails 3.0.19, and just seems completely wrong.

If I wanted to add a new empty object to the @story.tasks collection, then I would have done something like:

@story.tasks << @task

If I am assigning @story.tasks.new to a variable, I do not expect it to also add the retrieved object to the initial collection.

Could someone take a look at this and tell me the reasons for this change? Or whether it's a bug? If it's a new 'feature', then I will have to change my Rails 3 app (when I go up to Rail 4), so that all sections of code like the above work slightly differently.
For example, to stop this behaviour from happening in rails 4, you can do the following:

For example:

@tasks = @story.tasks.all
@task = @story.tasks.new

This doesn't 'fix' the issue, but at least means my @tasks variable has the expected number of results.

Just to reiterate, I would like to know why this is happening, before I am told how to change the code for it to work in Rails 4.

Thanks a lot for your time.

Neil

@pftg
pftg commented Oct 21, 2013

@nwillia2 GitHub issues for bugs only. For discussion please ask such questions on Ruby on Rails Mailing List.

By the way, as for me this behaviour more expectable then it was in 3.0.19.
To check: is this a feature, you can search through merged PR and re-read discussion.

@rafaelfranca
Member

This is expected behavior and it make sense to me. @story.tasks.new is the same as @story.tasks.build

@nwillia2

Thanks for your reply. I can appreciate your a busy person, but I have to ask, did you even read the post?

Why is this expected behaviour? I can't think of one example use for the way it now works in Rails 4.

Do you know by any chance when this behaviour changed? As I know there are A LOT of versions in between Rails 3.0.19 and 4.0.0.

Thanks.

@dmathieu

This behavior has happened for quite some time now. It did occur in 3.x.
While it's a bit boring sometimes, I also believe it to be the appropriate one.

@rafaelfranca
Member

@nwillia2 I read the issue. I usually read every comment on Issues/commits/PR on the Rails organization even if sometimes I didn't answer.

This behavior was added on 3.2.0, exactly by this commits eb7ef2c.

Why I think it is correct:

@task = @story.tasks.new
@story.tasks.include?(@task)

The current implementation will return true.

The precious implementation would return false and to make that code return true I have to:

@task = @story.tasks.new
@task.save
@store.reload
@story.tasks.include?(@task)

See I had to save and also reload the collection in my object to get that true.

This is why I think it is correct. Since I created my new object inside my collection I expect that my collection already includes the new object. If that is not what I want, I don't see why I'd call new in my collection when I can call Task.new to create an object.

I hope this help you and make things clear, if not, feel free to ask more question here.

@nwillia2

That's great clarification.

I would prefer to see something like @story.tasks.new! / @story.tasks.build! to do what it currently does. But that's just my opinion.

Thanks for your response, I now also consider this closed :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment