The 07/11/11, Dave Aronson wrote:

> The usual reason for using has_many :through (hmt) is that you need to
> attach some information to that relationship.  For instance, you may
> want to record exactly *when* a given Legal_representative started
> representing a given Company, how much they're charging, etc.  If you
> are absolutely sure you don't, and *never will*, need to attach any
> info to the relationship, then has_and_belongs_to_many (habtm) will do
> fine.
> 
> BUT:
> 
> 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

-- 
You received this message because you are subscribed to the Google Groups "Ruby 
on Rails: Talk" group.
To post to this group, send email to rubyonrails-talk@googlegroups.com.
To unsubscribe from this group, send email to 
rubyonrails-talk+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/rubyonrails-talk?hl=en.

Reply via email to