Delaying initialization of associations until first access


#1

Guys,

Say I have the following:

class Person < ActiveRecord::Base
has_one :house
end

class House < ActiveRecord::Base
belongs_to :person
attr_accessor :color
end

Then I have the following code:

john = Person.new
john.house.color = “Blue”
john.save

What I would like to have happen, is that on first call to john.house,
if
house hadn’t been initialized a House object would be created and
assigned
to the house field.

I’ve struggled to get this to work. Some have suggested overriding
Person#save to make sure a House record was created everytime I created
a
Person record, but I really, really don’t want to do this. If I have
100000 Person objects, and only 20% of them own a house, all those empty
House records in the database is a lot of waste.

I’ve tried variations on the below code, each of which has failed to
work.
It seemed to work, but it doesn’t persist to the database properly:

class Person < ActiveRecord::Base
has_one :house

def house
if !read_attribute(:house)
h = House.new
h.save
write_attribute(:house, h)
end
read_attribute(:house)
end
end

Can anyone tell me the proper way to accomplish this behavior?

Thanks!
John


#2

What exactly doesn’t get saved properly? From the looks of it you are
not setting the house’s person property, which leads me to think that
the person_id isn’t getting set in the database? Yes? No?

So, before your h.save call, just add a h.person = self. Or you could
just create and save all in one step. Your total method would be:
def house
return House.create(:person_id => self.id) if !read_attribute(:house)
read_attribute(:house)
end

Just have the create call, no need to write the attribute or assign it
anything else, everything should work just fine like that.

I think this will work, but its untested :wink:

-Nick


#3

Nick S. said:

So, before your h.save call, just add a h.person = self. Or you could
just create and save all in one step. Your total method would be:
def house
return House.create(:person_id => self.id) if !read_attribute(:house)
read_attribute(:house)
end

Yes, I’ve tried this in two forms:

def house
if !read_attribute(:house)
h = House.new
h.person = self

second form tried was

h.person_id = self.id

h.save
write_attribute(:house, h)
end
read_attribute(:house)
end

Both “seem” to work, and the House record is created. However, if I then
do:

someperson.house.color = “Red”
someperson.save

I look in my logs, and the person record is being updated but the house
record is not. Then, if I reload the person:

someperson = Person.find(1)
someperson.house.color
=> nil

Examining the logs, it seems that in “if !read_attribute(:house)”,
read_attribute(:house) never tests to true, even if the record already
exists. This is counter intuitive and bewildering.

Any ideas what I may be doing wrong?

Thanks!

John


#4

Nick S. said:

So, before your h.save call, just add a h.person = self. Or you could
just create and save all in one step. Your total method would be:
def house
return House.create(:person_id => self.id) if !read_attribute(:house)
read_attribute(:house)
end

Yes, I’ve tried this in two forms:

def house
if !read_attribute(:house)
h = House.new
h.person = self

second form tried was

h.person_id = self.id

h.save
write_attribute(:house, h)
end
read_attribute(:house)
end

Both “seem” to work, and the House record is created. However, if I then
do:

someperson.house.color = “Red”
someperson.save

I look in my logs, and the person record is being updated but the house
record is not. Then, if I reload the person:

someperson = Person.find(1)
someperson.house.color
=> nil

Examining the logs, it seems that in “if !read_attribute(:house)”,
read_attribute(:house) never tests to true, even if the record already
exists. This is counter intuitive and bewildering.

Any ideas what I may be doing wrong?

Thanks!

John


#5

On 2/6/06, John W. removed_email_address@domain.invalid wrote:

def house

Both “seem” to work, and the House record is created. However, if I then do:

someperson.house.color = “Red”
someperson.save

You are going to need to save your house model seperatly from your
person model. But! If you always want to do this, through in a
before_save filter for the person class that automatically saves the
house model at the same time. Should be simple enough.

exists. This is counter intuitive and bewildering.
For this, try using attribute_present? instead. Seems to be built
for what you are trying to do. Haven’t used it personally so not sure.
Just dug it out of the API.

Hope this helps!
-Nick


#6

On Monday, February 06, 2006, at 8:44 PM, Nick S. wrote:

You are going to need to save your house model seperatly from your
person model. But! If you always want to do this, through in a
before_save filter for the person class that automatically saves the
house model at the same time. Should be simple enough.

This part works…when I set the before_save filter, the House is indeed
saved to the database. However:

For this, try using attribute_present? instead. Seems to be built
for what you are trying to do. Haven’t used it personally so not sure.
Just dug it out of the API.

Switched it to attribute_present?..same behavior. Isn’t surprising
since attribute_present? uses read_attribute under the covers. So, while
we’re successful in writing to the database, when you reload the Person
object ActiveRecord for some reason doesn’t see the connection and
attribute_present? resolves false, therefore creating another entry in
the table.

I am at a complete loss.

The code now looks like this:

def before_save
self.house.save
end

def part_count
if !attribute_present?(“house”)
h = House.new
h.color = “blue”
h.person = self
write_attribute(“house”, h)
end
read_attribute(“house”)
end

Any other thoughts?


#7

On Monday, February 06, 2006, at 8:44 PM, Nick S. wrote:

You are going to need to save your house model seperatly from your
person model. But! If you always want to do this, through in a
before_save filter for the person class that automatically saves the
house model at the same time. Should be simple enough.

This part works…when I set the before_save filter, the House is indeed
saved to the database. However:

For this, try using attribute_present? instead. Seems to be built
for what you are trying to do. Haven’t used it personally so not sure.
Just dug it out of the API.

Switched it to attribute_present?..same behavior. Isn’t surprising
since attribute_present? uses read_attribute under the covers. So, while
we’re successful in writing to the database, when you reload the Person
object ActiveRecord for some reason doesn’t see the connection and
attribute_present? resolves false, therefore creating another entry in
the table.

I am at a complete loss.

The code now looks like this:

def before_save
self.house.save
end

def part_count
if !attribute_present?(“house”)
h = House.new
h.color = “blue”
h.person = self
write_attribute(“house”, h)
end
read_attribute(“house”)
end

Any other thoughts?


#8

Update…I have it working!

I’m not sure why this works and our previous attempts didn’t (would love
the explanation). greendale at rails.technoweenie.net provided this
code, which worked:

alias :_house :house

def house
_house or self.house = House.new
end

Now, that’s simple. I can’t understand when this worked and
read_attribute didn’t?