98

Using this modified example from the Rails guides, how does one model a relational "has_many :through" association using mongoid?

The challenge is that mongoid does not support has_many :through as ActiveRecord does.

# doctor checking out patient
class Physician < ActiveRecord::Base
 has_many :appointments
 has_many :patients, :through => :appointments
 has_many :meeting_notes, :through => :appointments
end
# notes taken during the appointment
class MeetingNote < ActiveRecord::Base
 has_many :appointments
 has_many :patients, :through => :appointments
 has_many :physicians, :through => :appointments
end
# the patient
class Patient < ActiveRecord::Base
 has_many :appointments
 has_many :physicians, :through => :appointments
 has_many :meeting_notes, :through => :appointments
end
# the appointment
class Appointment < ActiveRecord::Base
 belongs_to :physician
 belongs_to :patient
 belongs_to :meeting_note
 # has timestamp attribute
end
asked Aug 9, 2011 at 17:57

3 Answers 3

153

Mongoid doesn't have has_many :through or an equivalent feature. It would not be so useful with MongoDB because it does not support join queries so even if you could reference a related collection via another it would still require multiple queries.

https://github.com/mongoid/mongoid/issues/544

Normally if you have a many-many relationship in a RDBMS you would model that differently in MongoDB using a field containing an array of 'foreign' keys on either side. For example:

class Physician
 include Mongoid::Document
 has_and_belongs_to_many :patients
end
class Patient
 include Mongoid::Document
 has_and_belongs_to_many :physicians
end

In other words you would eliminate the join table and it would have a similar effect to has_many :through in terms of access to the 'other side'. But in your case thats probably not appropriate because your join table is an Appointment class which carries some extra information, not just the association.

How you model this depends to some extent on the queries that you need to run but it seems as though you will need to add the Appointment model and define associations to Patient and Physician something like this:

class Physician
 include Mongoid::Document
 has_many :appointments
end
class Appointment
 include Mongoid::Document
 belongs_to :physician
 belongs_to :patient
end
class Patient
 include Mongoid::Document
 has_many :appointments
end

With relationships in MongoDB you always have to make a choice between embedded or associated documents. In your model I would guess that MeetingNotes are a good candidate for an embedded relationship.

class Appointment
 include Mongoid::Document
 embeds_many :meeting_notes
end
class MeetingNote
 include Mongoid::Document
 embedded_in :appointment
end

This means that you can retrieve the notes together with an appointment all together, whereas you would need multiple queries if this was an association. You just have to bear in mind the 16MB size limit for a single document which might come into play if you have a very large number of meeting notes.

answered Aug 13, 2011 at 20:06

2 Comments

+1 very nice answer, just for info, mongodb size limit has been increased to 16 MB.
Out of curiosity (sorry for the late inquiry), I'm also new to Mongoid and I was wondering how you would query for data when it is an n-n relationship using a separate collection to store the association, is it the same as it was with ActiveRecord?
42

Just to expand on this, here's the models extended with methods that act very similar to the has_many :through from ActiveRecord by returning a query proxy instead of an array of records:

class Physician
 include Mongoid::Document
 has_many :appointments
 def patients
 Patient.in(id: appointments.pluck(:patient_id))
 end
end
class Appointment
 include Mongoid::Document
 belongs_to :physician
 belongs_to :patient
end
class Patient
 include Mongoid::Document
 has_many :appointments
 def physicians
 Physician.in(id: appointments.pluck(:physician_id))
 end
end
answered May 1, 2013 at 1:09

6 Comments

this surely helped cause my method for retrieving was returning an array which messed up pagination.
No magic. @CyrilDD, what are you referring to? map(&:physician_id) is short-hand for map{|appointment| appointment.physician.id}
I wonder, does this approach reduce the potential frustration with the 16MBs document size limit, given that the documents are not embedded but instead associated using an outside model? (sorry if this is a noob question!)
As Francis explains, using .pluck() sinstead of .map is MUCH faster. Can you update your answer for future readers ?
I'm getting undefined method 'pluck' for #<Array:...>
|
8

Steven Soroka solution is really great! I don't have the reputation to comment an answer(That's why I'm adding a new answer :P) but I think using map for a relationship is expensive(specially if your has_many relationship have hunders|thousands of records) because it gets the data from database, build each record, generates the original array and then iterates over the original array to build a new one with the values from the given block.

Using pluck is faster and maybe the fastest option.

class Physician
 include Mongoid::Document
 has_many :appointments
 def patients
 Patient.in(id: appointments.pluck(:patient_id))
 end
end
class Appointment
 include Mongoid::Document
 belongs_to :physician
 belongs_to :patient 
end
class Patient
 include Mongoid::Document
 has_many :appointments 
 def physicians
 Physician.in(id: appointments.pluck(:physician_id))
 end
end

Here some stats with Benchmark.measure:

> Benchmark.measure { physician.appointments.map(&:patient_id) }
 => #<Benchmark::Tms:0xb671654 @label="", @real=0.114643818, @cstime=0.0, @cutime=0.0, @stime=0.010000000000000009, @utime=0.06999999999999984, @total=0.07999999999999985> 
> Benchmark.measure { physician.appointments.pluck(:patient_id) }
 => #<Benchmark::Tms:0xb6f4054 @label="", @real=0.033517774, @cstime=0.0, @cutime=0.0, @stime=0.0, @utime=0.0, @total=0.0> 

I am using just 250 appointments. Don't forget to add indexes to :patient_id and :physician_id in Appointment document!

I hope it helps, Thanks for reading!

answered Feb 24, 2015 at 16:27

1 Comment

I'm getting undefined method 'pluck' for #<Array:...>

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.