Search code examples
ruby-on-railsrubyactiverecordmodelautosave

Ruby on Rails autosave associations


I have three associated classes in Rails v. 3.2.15, with Ruby 2.1.1, and a join-table class between two of them:

class Grandarent < ActiveRecord::Base
  has_many :parents, autosave: true
end

class Parent
  belongs_to :grandparent
  has_many :children, :through => :parent_children, autosave: true
end

class ParentChild
  belongs_to :parent
  belongs_to :child
end

class Child
  has_many :parent_children
  has_many :parents, :through => :parent_children
end

If I execute the following, then changes to child are not saved:

gp = Grandparent.find(1)
gp.parents.first.children.first.first_name = "Bob"
gp.save
gp.parents.first.children.first.first_name  ## -> Whatever name was to begin with (i.e. NOT Bob)

But if I force Rails to evaluate and return data from each connection, then the save is successful

gp = Grandparent.find(1)
gp.parents
gp.parents.first
gp.parents.first.children
gp.parents.first.children.first
gp.parents.first.children.first.first_name = "Bob"
gp.save
gp.parents.first.children.first.first_name  ## -> "Bob"

If I subsequently execute gp = Grandparent.find(1) again, then I've reset the whole thing, and have to force the evaluation of associations again.

Is this intentional behavior, or have I done something wrong? Do I need to hang an autosave on the join table connections as well as (or instead of) the has_many :through connection?

From the documentation, I see that "loaded" members will be saved. Is this what is necessary to load them? Can someone define exactly what "loaded" is, and how to achieve that state?


Solution

  • It is happening because gp.parents caches the parents into a results Array, then parents.first is actually calling Array.first. However, gp.parents.first performs a query with LIMIT 1 every time, and so returns a new object every time.

    You can confirm like so:

    gp.parents.first.object_id # performs new query (LIMIT 1)
    => 1
    
    gp.parents.first.object_id # performs new query (LIMIT 1)
    => 2
    
    gp.parents                 # performs and caches query for parents
    gp.parents.first.object_id # returns first result from parents array
    => 1
    
    gp.parents.first.object_id # returns first result from parents array
    => 1
    

    You can chain an update with your query like so:

    gp.parents.first.children.first.update_attributes(first_name: "Bob")