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

96

Используя этот модифицированный пример из руководств 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
Марио Зильотто
источник

Ответы:

151

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 МБ для одного документа, которое может вступить в силу, если у вас очень большое количество заметок к собранию.

Стив
источник
7
+1 очень хороший ответ, просто для информации, ограничение размера mongodb увеличено до 16 МБ.
Rubish
1
Из любопытства (извините за поздний запрос), я также новичок в Mongoid, и мне было интересно, как вы будете запрашивать данные, когда это отношение nn, используя отдельную коллекцию для хранения ассоциации, такая же, как и была с ActiveRecord?
innospark
38

Чтобы расширить это, вот модели, расширенные методами, которые действуют очень похоже на has_many: through из 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
Стивен Сорока
источник
2
это определенно помогло, потому что мой метод извлечения возвращал массив, который испортил нумерацию страниц.
prasad.surase
1
Никакой магии. @CyrilDD, о чем ты? карта (&: Physician_id) - это сокращение от карты {| встречи | assign.physician.id}
Стивен Сорока
Интересно, уменьшает ли этот подход потенциальное разочарование в связи с ограничением размера документа в 16 МБ, учитывая, что документы не встроены, а вместо этого связаны с использованием внешней модели? (извините, если это вопрос
новичков
Как объясняет Фрэнсис, использование .pluck()sinstead of намного .mapбыстрее. Можете ли вы обновить свой ответ для будущих читателей?
Сирил Дюшон-Дорис
Я получаюundefined method 'pluck' for #<Array:...>
Уиллиам Джадд
7

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

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

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 в документе о встрече!

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

Францискодельгадодев
источник
Я получаюundefined method 'pluck' for #<Array:...>
Уиллиам Джадд
0

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

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

Если мы используем СУБД и 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

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

Отношения «многие ко многим», в которых обратные документы хранятся в отдельной коллекции от базового документа, определяются с помощью макроса Mongoid has_and_belongs_to_many. Это демонстрирует поведение, аналогичное 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

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

Донато
источник
Примеры документов кажутся такими же?
CyberMew