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.