JJdinho
JJdinho

Reputation: 63

Multiple has and belongs to many relationship

I have a model that has a has_and_belongs_to_many relationship with two other models. For example, I have a Message model which has recipients. However, the recipients can be one of two different types of models. For example, InternalContact, and ExternalContact.

It is easy to set up relations where I can get either the Message's external contacts (Message.first.external_contacts) or its internal contacts (Message.first.internal_contacts). And then I can, in the same vein, easily add to this relationship array (Message.first.external_contacts << ExternalContact.first)

What I want is the ability to simply things and make this more efficiently by calling something like:

Message.first.recipients # an array of both external and internal contacts
Message.first.recipients << ExternalContact/InternalContact # (add either model)

Any direction is appreciated, I'm definitely stuck!

Upvotes: 0

Views: 814

Answers (3)

Vasfed
Vasfed

Reputation: 18454

With explicit has-and-belongs-to-many (aka has_many through:) relation model you'll have more control over the relation and can make one end of it polymorphic:

class Message < ApplicationRecord
  has_many :message_recipients
  has_many :internal_recipients, through: :message_recipients, source: :recipient, source_type: "InternalContact"
end

class InternalContact < ApplicationRecord
  has_many :message_recipients, as: :recipient
  has_many :messages, through: :message_recipients, inverse_of: :internal_recipients
end

class MessageRecipient < ApplicationRecord
  belongs_to :message
  belongs_to :recipient, polymorphic: true
end


msg = Message.create!
rec = InternalContact.create!
msg.internal_recipients << rec
msg.message_recipients.each{|mr|
  mr.recipient # here can be any model
}

Upvotes: 1

max
max

Reputation: 101831

One way to do this if you want to avoid a polymorphic association is to use Single Table Inheritance.

One major reason you would want to do this is to be able use a foreign key to ensure referential integrity which is not possible with polymorphism.

Since the types share a table you can also treat them like a homogeneous collection when it comes to ordering.

But single table inheritance is only really suited if the different types are very similar in nature and are interchangeable.

Setting up STI

First create a contacts table where you will store all the different types.

class CreateContacts < ActiveRecord::Migration[5.2]
  def change
    create_table :contacts do |t|
      t.string :type
      # and whatever columns you need 
      t.string :email
      # ...
      t.timestamps
    end
  end
end

Pay attention to the type column. ActiveRecord will use this to store the class name of the subclasses. Then setup InternalContact and ExternalContact to inherit from the base Contact class:

class InternalContact < Contact
  # ...
end

class ExternalContact < Contact
  # ...
end

This lets you implement whatever logic you want (such as validations and callbacks) for each subtype.

The associations

Start with the join table:

class CreateMessageRecipients < ActiveRecord::Migration[5.2]
  def change
    create_table :message_recipients do |t|
      t.belongs_to :message
      t.belongs_to :recipient, foreign_key: { to_table: :contacts }
      t.timestamps
    end
  end
end

Lets setup the associations with has_many through: and a join model instead of has_and_belongs_to_many which is very limited:

class Contact < ApplicationRecord
  has_many :message_recipients, foreign_key: :recipient_id
  has_many :messages, through: :message_recipients
end

class MessageRecipient < ApplicationRecord
  belongs_to :message
  belongs_to :recipient, class_name: 'Contact'
end

class Message < ApplicationRecord
  has_many :message_recipients
  has_many :recipients, through: :message_recipients
end

Upvotes: 1

Prasath Rajasekaran
Prasath Rajasekaran

Reputation: 375

You can have a separate recipient type table and then map messages only with recipients

Recipient          Recipient_type   Message       Message_recipient_map
----------         ---------------  -------       -------------------
id                 id               id            message_id  
recipient_type_id                                 recipient_id 

Then you can have has_many_through relationship between message and recipient and can access all recipients of the message by calling message.recipients

Hope this helps you!

Upvotes: 1

Related Questions