I was building a Rails app which required a Contacts model to be able to relate to many other contacts where the relationship was reciprocal.
The Database
Because contacts can have relationships with many other contacts I knew I needed another table to track this and knew I needed a has_many :through
association.
Rails has another association called has_and_belongs_to_many
which also could have worked but it’s not as easy to work with as it doesn’t have any intervening model and doesn’t offer flexibility like adding extra columns.
Creating the migration.
class CreateRelationships < ActiveRecord::Migration[5.0]
def change
create_table :relationships do |t|
t.integer :contact_id
t.integer :relation_id
t.timestamps
end
end
end
The associations
Next I needed to setup the associations.
# relationship.rb
class Relationship < ApplicationRecord
belongs_to :contact
belongs_to :relation, class_name: 'Contact'
end
# contact.rb
class Contact < ApplicationRecord
has_many :relationships
has_many :relations, through: :relationships
end
Great I could now create the relationships but the problem was the relationship would only be viewable from the contact who created the relationship and not the other way around.
So on the contact model I created an association for the inverse of the relationship.
# contact.rb
class Contact < ApplicationRecord
has_many :relationships
has_many :relations, through: :relationships
has_many :inverse_relationships, class_name: 'Relationship', foreign_key: 'relation_id'
has_many :inverse_relations, through: :inverse_relationships, source: :contact
end
But then to get both sides of a relationship I had to go through 2 associations or do lookups on both columns:
# using the associations
@contact.relationships
@contact.inverse_relationships
# or query the model for both sides
Relationship.where("contact_id = :contact OR relation_id = :contact", contact: @contact)
While this was worked it just didn’t seem efficient or elegant.
A better way
So I decided to refactor this inverse association and instead when a relationship is created I would use callback to create (and destroy) the inverse relationship in the database.
# relationship.rb
class Relationship < ApplicationRecord
belongs_to :contact
belongs_to :relation, class_name: 'Contact'
after_create :create_inverse, unless: :inverse_exists?
after_destroy :destroy_inverse, if: :inverse_exists?
def create_inverse
self.class.create(inverse_relationship)
end
def destroy_inverses
inverses.destroy_all
end
def inverse_exists?
self.class.exists?(inverse_relationship)
end
def inverses
self.class.where(inverse_relationship)
end
def inverse_relationship
{ relation_id: contact_id, contact_id: relation_id }
end
end
While this creates more database records in the relationships table the lookup queries are so much better now.