Rob
Rob

Reputation: 4434

rails associations :autosave doesn't seem to working as expected

I made a real basic github project here that demonstrates the issue. Basically, when I create a new comment, it is saved as expected; when I update an existing comment, it isn't saved. However, that isn't what the docs for :autosave => true say ... they say the opposite. Here's the code:

class Post < ActiveRecord::Base
  has_many :comments, 
           :autosave => true, 
           :inverse_of => :post,
           :dependent => :destroy

  def comment=(val)
    obj=comments.find_or_initialize_by(:posted_at=>Date.today)
    obj.text=val
  end
end

class Comment < ActiveRecord::Base
  belongs_to :post, :inverse_of=>:comments
end

Now in the console, I test:

p=Post.create(:name=>'How to groom your unicorn')
p.comment="That's cool!"
p.save!
p.comments # returns value as expected. Now we try the update case ... 

p.comment="But how to you polish the rainbow?"
p.save!
p.comments # oops ... it wasn't updated

Why not? What am I missing?

Note if you don't use "find_or_initialize", it works as ActiveRecord respects the association cache - otherwise it reloads the comments too often, throwing out the change. ie, this implementation works

def comment=(val)
  obj=comments.detect {|obj| obj.posted_at==Date.today}
  obj = comments.build(:posted_at=>Date.today) if(obj.nil?)
  obj.text=val
end

But of course, I don't want to walk through the collection in memory if I could just do it with the database. Plus, it seems inconsistent that it works with new object but not an existing object.

Upvotes: 5

Views: 1301

Answers (3)

John Naegle
John Naegle

Reputation: 8257

Here is another option. You can explicitly add the record returned by find_or_initialize_by to the collection if it is not a new record.

def comment=(val)
  obj=comments.find_or_initialize_by(:posted_at=>Date.today)
  unless obj.new_record?
    association(:comments).add_to_target(obj) 
  end
  obj.text=val
end

Upvotes: 3

Yevgeniy Goyfman
Yevgeniy Goyfman

Reputation: 492

John Naegle is right. But you can still do what you want without using detect. Since you are updating only today's comment you can order the association by posted_date and simply access the first member of the comments collection to updated it. Rails will autosave for you from there:

class Post < ActiveRecord::Base
  has_many :comments, ->{order "posted_at DESC"}, :autosave=>true,     :inverse_of=>:post,:dependent=>:destroy

  def comment=(val)
    if comments.empty? || comments[0].posted_at != Date.today
      comments.build(:posted_at=>Date.today, :text => val)
    else
      comments[0].text=val
    end
  end
end

Upvotes: 0

John Naegle
John Naegle

Reputation: 8257

I don't think you can make this work. When you use find_or_initialize_by it looks like the collection is not used - just the scoping. So you are getting back a different object.

If you change your method:

  def comment=(val)
    obj = comments.find_or_initialize_by(:posted_at => Date.today)
    obj.text = val
    puts "obj.object_id: #{obj.object_id} (#{obj.text})"
    puts "comments[0].object_id: #{comments[0].object_id} (#{comments[0].text})"
    obj.text
  end

You'll see this:

p.comment="But how to you polish the rainbow?"
obj.object_id: 70287116773300 (But how to you polish the rainbow?)
comments[0].object_id: 70287100595240 (That's cool!)

So the comment from find_or_initialize_by is not in the collection, it outside of it. If you want this to work, I think you need to use detect and build as you have in the question:

  def comment=(val)
    obj = comments.detect {|c| c.posted_at == Date.today } || comments.build(:posted_at => Date.today)
    obj.text = val
  end

Upvotes: 1

Related Questions