in Hacking

has_many tricky replace method

Today I discovered a small bug / feature of rails 3.1.3.

Having the following structure:

class Item < ActiveRecord::Base
  has_many :tags

class Tag < ActiveRecord::Base
  # Tag has a boolean flag 'enabled'
  belongs_to :item

It's possible to completely replace the has_many collection
with a new collection. Make this collection:

item = Item.find(1)
tags = [ 
  item.tags.create( :enabled => true, :name => "tag1" )

All fine for the moment. As expected the enabled flag of the first item is set:

tags[0].name      # => "tag1"
tags[0].enabled?  # => true

Now replacing the existing tags:

item.tags.replace( tags )
item.tags[0].name        # => "tag1"
item.tags[0].enabled?    # => false 

What?? It just forgot the enabled flag!!

The way to replace the items is by assigning:

item.tags = tags
item.tags[0].name        # => "tag1"
item.tags[0].enabled?    # => true

So remember when replacing collections of has_many do not replace them but assign them....

  1. I found this in the Rails source, in collection_association.rb

    Let’s play “spot the bug”; I don’t see it, do you?

    305 # Replace this collection with +other_array+
    306 # This will perform a diff and delete/add only records that have changed.
    307 def replace(other_array)
    308 other_array.each { |val| raise_on_type_mismatch(val) }
    309 original_target = load_target.dup
    311 if owner.new_record?
    312 replace_records(other_array, original_target)
    313 else
    314 transaction { replace_records(other_array, original_target) }
    315 end
    316 end

    • Wel I also checked the rails code and it seems the assign operation is identical to the replace method :S

      def writer(records)

      Strange! I guess I need to review recheck my own code again! Thanks for the response!

Comments are closed.