2

I have the following:

  has_many :matches_as_mentor, foreign_key: :mentor_id, class_name: 'Match'
  has_many :matches_as_mentee, foreign_key: :mentee_id, class_name: 'Match'

and I need to have an association :matches that is made up of both of these combined together. what is a good way to do this?

2 Answers2

0

You have several different ways to achieve this. First one is simple

[*entity.matches_as_mentor, *entity.matches_as_mentee]

Second one is a little bit complex. You need to use UNION or UNION ALL sql statement depends on your case. Here is the difference - What is the difference between UNION and UNION ALL?.

What you need is just build a SQL query to get these records.

Third one is to make include. Entity.includes(:matches_as_mentor, :matches_as_mentee).

Depends on how you need to process(show) it and how much records do you have in these tables you need which one to choose.

I prefer to use UNION ALL if you don't need duplicates. But also you can look for INNER JOIN.

One thing that i want to add is you can do smth like that

Match.where(mentor_id: entity.id).or(Match.where(mentee_id: entity.id)) 

The last one approach gives you the best perfomance.

CR7
  • 1,056
  • 1
  • 8
  • 18
  • 1
    Using array concatenation isn't great as you can't order the results or use limits or actually do anything meaningful with the data. `Match.where(mentor_id: entity.id).or(Match.where(mentee_id: entity.id))` is a lot better alternative if you want a quick an easy fix. `Entity.joins(:matches_as_mentor, :matches_as_mentee)` actually gives a list of entities so you can scratch that off the list. – max May 06 '21 at 16:43
  • @max Yea, i really forgot about where condition, nice catch. You are right that joins gives a list of entities but it also preload your relations. And you can easily grab their attributes.Obviously that u will not have an array of matches but you will have the preloaded relations which can be mapped through entity objects. – CR7 May 06 '21 at 17:34
  • `.joins` *does not* preload assocations. It just adds an `INNER JOIN` to the query. You're thinking of `.eager_load`, `.includes` or `.preload` in which case you have the same issue as with array concatenation as its not a homegenous collection. – max May 06 '21 at 17:39
  • @max actually you don't need everytime to return ActiveRecord::Relation. Preferably but it depends on a case. – CR7 May 06 '21 at 17:42
  • 1
    @CR7 a scope ("an association") should always return an `ActiveRecord::Relation` because scopes are intended to be chainable. – engineersmnky May 06 '21 at 18:19
0

Unfortunely this is not really a case that ActiveRecord assocations really can handle that well. Assocations just link a single foreign key on one model to a primary key on another model and you can't just chain them together.

If you wanted to join both you would need the following JOIN clause:

JOINS matches ON matches.mentor_id = entities.id 
              OR matches.mentee_id = entities.id

You just can't get that as assocations are used in so many ways and you can't just tinker with the join conditons. To create a single assocation that contains both categories you need a join table:

Database diagram

class Entity < ApplicationRecord

  # Easy as pie
  has_many :matchings
  has_many :matches, through: :matchings

  # this is where it gets crazy
  has_many :matchings_as_mentor, 
     class_name: 'Matching',
     ->{ where(matchings: { role: :mentor }) }

  has_many :matches_as_mentor, 
     class_name: 'Match',
     through: :matchings_as_mentor

  has_many :matchings_as_mentee, 
     class_name: 'Matching',
     ->{ where(matchings: { role: :mentee }) }

  has_many :matches_as_mentee, 
     class_name: 'Match',
     through: :matchings_as_mentor
end

class Matching < ApplicationRecord
  enum role: [:mentor, :mentee]
  belongs_to :entity
  belongs_to :match
  validates_uniqueness_of :entity_id, scope: :match_id
  validates_uniqueness_of :match_id, scope: :role
end

class Match < ApplicationRecord
  # Easy as pie
  has_many :matchings
  has_many :entities, through: :matching

  # this is where it gets crazy
  has_one :mentor_matching, 
         class_name: 'Matching',
         ->{ where(matchings: { role: :mentee }) }
  has_one :mentor, through: :mentor_matching, source: :entity

  has_one :mentee_matching, 
         class_name: 'Matching',
         ->{ where(matchings: { role: :mentor }) }
  has_one :mentee, through: :mentor_matching, source: :entity
end

And presto you can reap the rewards of having a homogenous association:

entities = Entity.includes(:matches)
entities.each do |e|
  puts e.matches.order(:name).inspect
end 
      

It is a lot more complex though and you need validations and constraints to ensure that a match can only have one mentor and mentee. its up to you to evaluate if its worth it.

The alternative is doing somehing like:

Match.where(mentor_id: entity.id)
     .or(Match.where(mentee_id: entity.id)) 

Which cannot does not allow eager loading so it would cause a N+1 query issue if you are displaying a list of entites and their matches.

max
  • 96,212
  • 14
  • 104
  • 165