Как реализовать has_many: через отношения с Mongoid и mongodb?

Используя этот измененный пример из руководств Rails, как можно моделировать реляционную ассоциацию has_many:through с использованием mongoid?

Проблема в том, что mongoid не поддерживает has_many:through, как ActiveRecord.

# 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

4 ответа

Решение

Mongoid не имеет has_many:through или эквивалентной функции. Это не было бы так полезно с MongoDB, потому что он не поддерживает запросы на соединение, поэтому даже если вы могли бы ссылаться на связанную коллекцию через другую, она все равно требовала бы нескольких запросов.

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

Обычно, если у вас есть отношение многие-многие в СУБД, вы должны смоделировать это по-разному в MongoDB, используя поле, содержащее массив "внешних" ключей с обеих сторон. Например:

class Physician
  include Mongoid::Document
  has_and_belongs_to_many :patients
end

class Patient
  include Mongoid::Document
  has_and_belongs_to_many :physicians
end

Другими словами, вы бы исключили объединяющую таблицу, и это имело бы эффект, аналогичный has_many:through с точки зрения доступа к "другой стороне". Но в вашем случае это, вероятно, не подходит, потому что ваша таблица соединений - это класс Appointment, который несет некоторую дополнительную информацию, а не только связь.

То, как вы смоделируете это, зависит в некоторой степени от запросов, которые вам нужно выполнить, но кажется, что вам нужно будет добавить модель назначений и определить связи с пациентом и врачом примерно так:

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

Со связями в MongoDB вы всегда должны выбирать между встроенными или связанными документами. В вашей модели я бы предположил, что MeetingNotes - хороший кандидат для встроенных отношений.

class Appointment
  include Mongoid::Document
  embeds_many :meeting_notes
end

class MeetingNote
  include Mongoid::Document
  embedded_in :appointment
end

Это означает, что вы можете извлекать заметки вместе с собранием вместе, тогда как вам понадобится несколько запросов, если это была связь. Вы просто должны иметь в виду ограничение размера в 16 МБ для одного документа, который может вступить в игру, если у вас очень большое количество заметок о собрании.

Просто чтобы расширить это, вот модели, дополненные методами, которые очень похожи на has_many: через ActiveRecord, возвращая прокси-запрос вместо массива записей:

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

Решение Стивена Сороки действительно великолепно! У меня нет репутации, чтобы комментировать ответ (вот почему я добавляю новый ответ:P), но я думаю, что использование map для отношений стоит дорого (особенно, если у ваших отношений has_many есть hunders | тысячи записей), потому что они получают Данные из базы данных, построить каждую запись, генерирует исходный массив и затем перебирает исходный массив, чтобы построить новый со значениями из данного блока.

Использование срывать быстрее и, возможно, самый быстрый вариант.

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

Вот немного статистики с 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> 

Я использую только 250 встреч. Не забудьте добавить индексы для: Patient_id и: Physician_id в документе назначения!

Я надеюсь, что это помогает, спасибо за чтение!

Я хочу ответить на этот вопрос с точки зрения самоссылающейся ассоциации, а не только с точки зрения has_many:through.

Допустим, у нас есть CRM с контактами. Контакты будут иметь отношения с другими контактами, но вместо того, чтобы создавать отношения между двумя разными моделями, мы будем создавать отношения между двумя экземплярами одной и той же модели. У контакта может быть много друзей, и он может быть подружен со многими другими контактами, поэтому нам придется создавать отношения многие-ко-многим.

Если мы используем RDBMS и ActiveRecord, мы бы использовали has_many:through. Таким образом, нам нужно будет создать модель соединения, такую ​​как Дружба. Эта модель будет иметь два поля: contact_id, который представляет текущего контакта, который добавляет друга, и friend_id, который представляет пользователя, с которым подружился.

Но мы используем MongoDB и Mongoid. Как указано выше, Mongoid не имеет has_many:through или эквивалентной функции. Это не было бы так полезно с MongoDB, потому что он не поддерживает запросы соединения. Поэтому, чтобы смоделировать отношение многие-многие в базе данных, не являющейся СУБД, такой как MongoDB, вы используете поле, содержащее массив "внешних" ключей с обеих сторон.

class Contact
  include Mongoid::Document
  has_and_belongs_to_many :practices
end

class Practice
  include Mongoid::Document
  has_and_belongs_to_many :contacts
end

Как указано в документации:

Отношения многие ко многим, в которых обратные документы хранятся в отдельной коллекции из базового документа, определяются с помощью макроса has_and_belongs_to_many Mongoid. Это демонстрирует поведение, аналогичное Active Record, за исключением того, что не требуется никакой коллекции соединений, идентификаторы внешнего ключа хранятся в виде массивов по обе стороны от отношения.

При определении отношения такого характера каждый документ сохраняется в соответствующей коллекции, и каждый документ содержит ссылку "внешний ключ" на другой в виде массива.

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

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

Теперь для самообращающейся ассоциации в MongoDB у вас есть несколько вариантов.

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

В чем разница между связанными контактами и контактами, имеющими много и принадлежащими ко многим практикам? Огромная разница! Одним из них является отношения между двумя сущностями. Другое - это ссылка на себя.

Другие вопросы по тегам