Has_and_belongs_to_many or has_many :through?

I always has doubts on using has_and_belongs_to_many rather than
has_many :through association.
I have a model like this:

class Company < ActiveRecord::Base
has_many :categories
has_many :legal_representatives
has_many :tenders
has_many :documents
end

I don’t see any way to use another model for these associations, do
you think it’s better to use has_many :through anyway?

On 7 November 2011 12:15, Mauro [email protected] wrote:

I don’t see any way to use another model for these associations, do
you think it’s better to use has_many :through anyway?

I don’t understand what you are asking, you do not appear to be using
habtm or has many through. Perhaps you can clarify the question
further

Colin

On Nov 7, 2011, at 1:26 PM, Mauro wrote:

has_many :documents
LegaRepresentative that has many companies.
In this case you use a has_and_belongs_to_many or has_many :through?
What’s reasoning that leads me to select has_and_belongs_to_many
rather than has_many :through?

You choose has_many :through when you need to “decorate” the junction
between the two models. Let’s say that you needed to capture the date
that a particular legal representative entered into an agreement with a
company. You can’t capture that in the strict habtm relationship –
that’s just a “dumb” join table and can only store the fact that the
relationship exists.

Walter

On 7 November 2011 13:43, Colin L. [email protected] wrote:

end

I don’t see any way to use another model for these associations, do
you think it’s better to use has_many :through anyway?

I don’t understand what you are asking, you do not appear to be using
habtm or has many through. Perhaps you can clarify the question
further

For example: I have a Company with many legal_representatives and a
LegaRepresentative that has many companies.
In this case you use a has_and_belongs_to_many or has_many :through?
What’s reasoning that leads me to select has_and_belongs_to_many
rather than has_many :through?

On 7 November 2011 19:32, Dave A.
[email protected] wrote:

So, aside from declaring one more class, there’s no reason not to use
hmt from the start.

Ok, that’s good, if I need or not to attach information to the
relationship I can always use has_many :through.
Can you suggest a name for the :through model between Company and
LegalRepresentative?

On Mon, Nov 7, 2011 at 13:26, Mauro [email protected] wrote:

For example: I have a Company with many legal_representatives and a
LegaRepresentative that has many companies.
In this case you use a has_and_belongs_to_many or has_many :through?
What’s reasoning that leads me to select has_and_belongs_to_many
rather than has_many :through?

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.

-Dave


LOOKING FOR WORK! What: Ruby (on/off Rails), Python, other modern
languages.
Where: Northern Virginia, Washington DC (near Orange Line), and remote
work.
See: davearonson.com (main) * codosaur.us (code) * dare2xl.com
(excellence).
Specialization is for insects. (Heinlein) - Have Pun, Will Babble!
(Aronson)

On Mon, Nov 7, 2011 at 13:53, Mauro [email protected] wrote:

Can you suggest a name for the :through model between Company and
LegalRepresentative?

Off the top of my head, maybe LegalRepresentation? Or since it’s so
close to an existing class, maybe just Representation? (Unless the
Company needs other forms of Representation.)

-Dave


LOOKING FOR WORK! What: Ruby (on/off Rails), Python, other modern
languages.
Where: Northern Virginia, Washington DC (near Orange Line), and remote
work.
See: davearonson.com (main) * codosaur.us (code) * dare2xl.com
(excellence).
Specialization is for insects. (Heinlein) - Have Pun, Will Babble!
(Aronson)

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