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