Model with two Value Object associations

I’m having trouble with this model:

class Person < AR::B
belongs_to :shipping_address, :class_name => ‘Address’
belongs_to :billing_address, :class_name => ‘Address’
end

class Address < AR::B
end

The problem is that I want Address to be a Value Object (as in DDD),
and if I do this:
p = Person.create :shipping_address => Address.new(…)

and later I change the address:
p.shipping_address = Address.new(…)
p.save

the first address object doesn’t get deleted from the DB. It becomes
an orphan.

I can inverse the association and use a has_one but then I have to put
two foreign keys in the address table… and that could be a problem
because there are other models that have addresses.

Another option could be to model both associations as composed_of but
then I have to put all of the address table columns on the people
table, and repeat this on the other models that have addresses too.

How can I solve this? Any suggestions?
Thanks in advance.

Hello, I think, you can to do it by using inheritance and STI for
Address model

class Person < AR::B
has_one :shipping_address, :class_name => ‘Address’
has_one :billing_address, :class_name => ‘Address’
end

class Address < AR::B
belongs_to :person
end

class ShippingAddress < Address
end

class BillingAddress < Address
end

and add ‘type’ column to address table

But if you want to use one address for shipping and billing, it may be
hard in this way.

shouldn’t it be:

class Person < AR::B
has_one :shipping_address, :class_name => ‘ShippingAddress’
has_one :billing_address, :class_name => ‘BillingAddress’
end

?

Marnen Laibow-Koser wrote:

If the DB
supported Address columns, of course you’d do it that way; since it
doesn’t, composed_of fakes this functionality for you.

It occurs to me that you actually can do this with a single address
column in the DB – just use serialize. And that way you don’t have to
worry about a repetitive schema.

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

Emma wrote:
[…]

if I do this:
p = Person.create :shipping_address => Address.new(…)

and later I change the address:
p.shipping_address = Address.new(…)
p.save

the first address object doesn’t get deleted from the DB. It becomes
an orphan.

Right – because there’s nothing in your code saying that the first
address should be deleted. How is Rails to know that you don’t want to
have the first Address available?

[…]

Another option could be to model both associations as composed_of but
then I have to put all of the address table columns on the people
table, and repeat this on the other models that have addresses too.

I think this is actually the correct approach. As far as the schema is
concerned, you don’t really want a separate Address table. If the DB
supported Address columns, of course you’d do it that way; since it
doesn’t, composed_of fakes this functionality for you. You can use
modules or AR subclasses to cut down on code repetition.

Alternatively, you could write a method that creates a new Address and
deletes the old one explicitly.

How can I solve this? Any suggestions?
Thanks in advance.

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

On Saturday 25 July 2009, Emma wrote:

The problem is that I want Address to be a Value Object (as in DDD),
and if I do this:
p = Person.create :shipping_address => Address.new(…)

and later I change the address:
p.shipping_address = Address.new(…)
p.save

the first address object doesn’t get deleted from the DB. It becomes
an orphan.

I’d try to handle a case like this in a callback. ActiveRecord
automatically generates methods like #shipping_address_id_changed?, but
that does only half the job in this particular case, because assigning
an unsaved object (such as Address.new) to a belongs_to association does
not change the existing foreign key immediately.

class Person < AR::B
belongs_to :shipping_address, :class_name => ‘Address’
belongs_to :billing_address, :class_name => ‘Address’

protected

def before_save
if shipping_address_id_changed? ||
shipping_address && (shipping_address_id != shipping_address.id)
Address.delete(shipping_address_id_was) if shipping_address_id_was
end
# same for billing_address; better extract the common code
end
end

The code probably won’t work as is, but it might get you started. Also,
have a look at the :autosave option for belongs_to.

HTH,
Michael


Michael S.
mailto:[email protected]
http://www.schuerig.de/michael/

You can also use the methods included from ActiveRecord::Dirty on the
foreign key fields; in your case, you’d have an after_save callback
like this (on Person):

after_save :cleanup_addresses

def cleanup_addresses
Address.destroy(shipping_address_id_was) if
shipping_address_id_changed? && shipping_address_id_was
Address.destroy(billing_address_id_was) if
billing_address_id_changed? && billing_address_id_was
end

Some notes on this:

  • if you don’t have any callbacks or observers on Address, you can
    simplify the .destroy calls to .delete, and save instantiating some
    Address objects.
  • if your UI allows users to swap the addresses (ie,
    shipping_address_id is swapped with billing_address_id, without any
    new DB records), you’ll need to have a better check in
    cleanup_addresses; the current code will end up deleting both
    addresses in that case.

–Matt J.

I’m sure - I’ve got a big chunk of code using _changed? in after_save
out in production. The flags are reset after the save operation
completes - note that you can still rollback a save in the after_save
callbacks by raising an exception.

–Matt J.

Guys, thank you very much for all your answers!

I finally used Matt’s solution because the Address model have an
association.
It works great.

On Saturday 25 July 2009, Matt J. wrote:

billing_address_id_changed? && billing_address_id_was
end

Matt, have you tried this code? Specifically, are you sure that the
dirty states have not already been reset by the time the after_save
callback is invoked? That was my concern when I suggested using a
before_save callback in a parallel post.

Michael


Michael S.
mailto:[email protected]
http://www.schuerig.de/michael/