How to implement has_many :through relationships with Mongoid and mongodb?

前端 未结 4 730
猫巷女王i
猫巷女王i 2020-11-29 15:55

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 doe

相关标签:
4条回答
  • 2020-11-29 16:36

    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.

    0 讨论(0)
  • 2020-11-29 16:44

    I want to answer this question from the self-referencing association perspective, not just the has_many :through perspective.

    Let's say we have a CRM with contacts. Contacts will have relationships with other contacts, but instead of creating a relationship between two different models, we’ll be creating a relationship between two instances of the same model. A contact can have many friends and be befriended by many other contacts so we’re going to have to create a many-to-many relationship.

    If we are using a RDBMS and ActiveRecord, we would use has_many :through. Thus we would need to create a join model, like Friendship. This model would have two fields, a contact_id that represents the current contact who’s adding a friend and a friend_id that represents the user who’s being befriended.

    But we are using MongoDB and Mongoid. As stated above, 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. Therefore, in order to model a many-many relationship in a non-RDBMS database like MongoDB, you use a field containing an array of 'foreign' keys on either side.

    class Contact
      include Mongoid::Document
      has_and_belongs_to_many :practices
    end
    
    class Practice
      include Mongoid::Document
      has_and_belongs_to_many :contacts
    end
    

    As the documentation states:

    Many to many relationships where the inverse documents are stored in a separate collection from the base document are defined using Mongoid’s has_and_belongs_to_many macro. This exhibits similar behavior to Active Record with the exception that no join collection is needed, the foreign key ids are stored as arrays on either side of the relation.

    When defining a relation of this nature, each document is stored in its respective collection, and each document contains a “foreign key” reference to the other in the form of an array.

    # the contact document
    {
      "_id" : ObjectId("4d3ed089fb60ab534684b7e9"),
      "practice_ids" : [ ObjectId("4d3ed089fb60ab534684b7f2") ]
    }
    
    # the practice document
    {
      "_id" : ObjectId("4d3ed089fb60ab534684b7e9"),
      "contact_ids" : [ ObjectId("4d3ed089fb60ab534684b7f2") ]
    }
    

    Now for a self-referencing Association in MongoDB, you have a few options.

    has_many :related_contacts, :class_name => 'Contact', :inverse_of => :parent_contact
    belongs_to :parent_contact, :class_name => 'Contact', :inverse_of => :related_contacts
    

    What is difference between related contacts and contacts having many and belonging to many practices? Huge difference! One is a relationship between two entities. Other is a self-reference.

    0 讨论(0)
  • 2020-11-29 16:48

    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
    
    0 讨论(0)
  • 2020-11-29 16:48

    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!

    0 讨论(0)
提交回复
热议问题