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.
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.
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.
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, 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.
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.