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

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

Проблема заключается в том, что mongoid не поддерживает has_many: через 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

Ответ 1

Mongoid не имеет has_many: через или эквивалентную функцию. Это не было бы так полезно с 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: через доступ к "другой стороне". Но в вашем случае это, вероятно, не подходит, потому что ваша таблица соединений - это класс 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 МБ для одного документа, который может вступить в игру, если у вас очень большое количество заметок.

Ответ 2

Просто чтобы расширить это, здесь модели расширены с помощью методов, которые очень похожи на 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

Ответ 3

Решение Стивена Сороки действительно здорово! У меня нет репутации, чтобы прокомментировать ответ (вот почему я добавляю новый ответ: 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 в документе Назначения!

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