The 07/11/11, Dave A. wrote:
First, but I’ve heard some people complain about difficulty getting
that working (I haven’t, and don’t know what they were doing wrong),
and secondly, if you’re wrong, I’ve heard that it’s very difficult to
retrofit an existing habtm setup to become hmt (haven’t tried to do
that myself).
So, aside from declaring one more class, there’s no reason not to use
hmt from the start.
I’ve also tried hmt and I find it too much complicated in practice. My
use
case is not exactly the same and starts from the simplest relationship.
Say you have customers doing orders for meals we’ll have to bill for. We
start
from models like this:
+-----------------------+
|Customer |
|has_one :location |
|has_many :orders |
|has_many :bills |
+-----------------------+
+-----------------------+
|Location |
|belongs_to :customer |
+-----------------------+
+-----------------------+
|Order |
|belongs_to :customer |
+-----------------------+
+-----------------------+
|Bill |
|belongs_to :customer |
+-----------------------+
Everythings goes fine… until customer Bob changes of home. If you want
to
take new orders with the new location, you just need to update the Bob’s
location which is WRONG because all previous recorded bills will
change of
location too.
The common answer in RoR is to give the location a new kind of relation
depending of a duration. Doing so, you may change your models to
+------------------------------------------------------+
|Customer |
|has_many :location, :through => :customer_locations |
| |
|has_many :orders |
|has_many :bills |
+------------------------------------------------------+
+------------------------------------------------------+
|Location |
|has_many :customers, :through => :customer_locations |
+------------------------------------------------------+
+------------------------------------------------------+
|CustomerLocation |
|belongs_to :customer |
|belongs_to :location |
+------------------------------------------------------+
where CustomerLocation has datetimes to define the relation validity. So
you’ll
have to define when a relation is valid for both the current and old
relations
depending on the datetimes. This led to a lot of complexity to handle
all cases
in the code. Also, it’s going to be even more complex if a customer may
have
diets, categories, etc that can change over time while you need to track
correct
history.
This is why I decided to take a new approach. I’ve written a plugin (not
public,
I don’t even know if someone else would be interested).
With the ContextFriendly plugin, I give the Customer and Location models
a
context. Given the original models, it is as simple as
+--------------------------+
|Customer |
|has_one :location |
|acts_as_context_friendly |
+--------------------------+
+--------------------------+
|Location |
|belongs_to :customers |
|acts_as_context_friendly |
+--------------------------+
Using the script from this plugin I can automatically create the
migrations
and models in the contexts “order” and “bill”. In this case, it will add
the
following models:
+-----------------------------+
|BillCustomer |
|has_one :bill_location |
|acts_as_context_friendly |
| |
|has_many :bills |
+-----------------------------+
+-----------------------------+
|OrderCustomer |
|has_one :order_location |
|acts_as_context_friendly |
| |
|has_many :orders |
+-----------------------------+
+-----------------------------+
|BillsLocation |
|belongs_to :bill_customer |
|acts_as_context_friendly |
+-----------------------------+
+-----------------------------+
|OrderLocation |
|belongs_to :order_customer |
|acts_as_context_friendly |
+-----------------------------+
Though, I have to manually check the models, migrations and relations
for
the contexts (the script creating them is really simple).
An abstract of this approach could be:
+-----------------+
±----------------+
| orders | | bills
|
±----------------+
±----------------+
| |
| |
v v
±----------------+ ±----------------+
±----------------+
|Context: none | |Context: order | |Context: bill |
±----------------+ ±----------------+
±----------------+
| Customer | | Customer | | Customer |
| Location | | Location | | Location |
±----------------+ ±----------------+
±----------------+
This is why I add the new models composed by the context name and the
reference
model name. Now, the original context (none) can be understood as the
“configuration records” from which I will create new records in the
“order” (or
any other) context. The “none” context is also called “reference”.
To register a new order for Bob, we only need to save Bob in the “order”
context:
bob = Customer.where('name like ?', 'Bob')
bob_in_order = OrderCustomer.new
bob_in_order <= bob # Change Bob instance of context
bob_in_order.save
or in a simpler form:
bob = Customer.where('name like ?', 'Bob')
bob.save_as(:OrderCustomer)
If Bob is already in the “order” context, nothing else is done at this
stage: no
new record is saved.
Also, when we want to add Bob from the “order” context to “bill” we can
do:
order_customer = OrderCustomer.where('name like ?',
‘Bob’).last
bill_customer = BillCustomer.new
bill_customer <= order_customer # Change context from
“order” to “bill”
bill_customer.save
or simply
OrderCustomer.where('name like ?',
‘Bob’).last.save_as(:BillCustomer)
I’ve implemented other usefull methods such as
Model.foreach_model_in_all_contexts()
Model.foreach_model_in_other_contexts()
or
model_instance.context() # Give informations like context and
reference names.
With this approach, I fall back to the simplest relationships for models
with the drawback of a lot more models. I find it much convenient to
deal with these models than with hmt in practice, though.
–
Nicolas Sebrecht