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

?

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,

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

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 Jones

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

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 Jones

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.