¿Cómo implementar has_many: a través de relaciones con Mongoid y mongodb?

96

Usando este ejemplo modificado de las guías de Rails , ¿cómo se modela una asociación relacional "has_many: through" usando mongoid?

El desafío es que mongoid no admite has_many: through como lo hace 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
Mario Zigliotto
fuente

Respuestas:

151

Mongoid no tiene has_many: through o una característica equivalente. No sería tan útil con MongoDB porque no admite consultas de combinación, por lo que incluso si pudiera hacer referencia a una colección relacionada a través de otra, aún requeriría múltiples consultas.

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

Normalmente, si tiene una relación de muchos a muchos en un RDBMS, lo modelaría de manera diferente en MongoDB utilizando un campo que contiene una matriz de claves 'externas' en cada lado. Por ejemplo:

class Physician
  include Mongoid::Document
  has_and_belongs_to_many :patients
end

class Patient
  include Mongoid::Document
  has_and_belongs_to_many :physicians
end

En otras palabras, eliminaría la tabla de combinación y tendría un efecto similar a has_many: through en términos de acceso al 'otro lado'. Pero en su caso, eso probablemente no sea apropiado porque su tabla de combinación es una clase de cita que contiene información adicional, no solo la asociación.

La forma en que modele esto depende en cierta medida de las consultas que necesita ejecutar, pero parece que necesitará agregar el modelo de Cita y definir asociaciones para Paciente y Médico algo como esto:

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

Con las relaciones en MongoDB, siempre tiene que elegir entre documentos integrados o asociados. En su modelo, supongo que MeetingNotes es un buen candidato para una relación integrada.

class Appointment
  include Mongoid::Document
  embeds_many :meeting_notes
end

class MeetingNote
  include Mongoid::Document
  embedded_in :appointment
end

Esto significa que puede recuperar las notas junto con una cita, mientras que necesitaría varias consultas si se tratara de una asociación. Solo debe tener en cuenta el límite de tamaño de 16 MB para un solo documento que podría entrar en juego si tiene una gran cantidad de notas de reunión.

Steve
fuente
7
+1 muy buena respuesta, solo para información, el límite de tamaño de mongodb se ha aumentado a 16 MB.
rubish
1
Por curiosidad (perdón por la consulta tardía), también soy nuevo en Mongoid y me preguntaba cómo consultaría datos cuando se trata de una relación nn utilizando una colección separada para almacenar la asociación, ¿es lo mismo que era? con ActiveRecord?
innospark
38

Solo para ampliar esto, aquí están los modelos extendidos con métodos que actúan de manera muy similar a has_many: a través de ActiveRecord al devolver un proxy de consulta en lugar de una matriz de registros:

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
Steven Soroka
fuente
2
esto seguramente ayudó a que mi método de recuperación devolviera una matriz que estropeaba la paginación.
prasad.surase
1
Sin magia. @CyrilDD, ¿a qué te refieres? map (&: doctor_id) es una abreviatura de map {| cita | cita.physician.id}
Steven Soroka
Me pregunto, ¿este enfoque reduce la frustración potencial con el límite de tamaño del documento de 16 MB, dado que los documentos no están incrustados sino asociados mediante un modelo externo? (¡Lo siento si esta es una pregunta de novato!)
Attila Györffy
Como explica Francis, usar .pluck()sin en lugar de .mapes MUCHO más rápido. ¿Puede actualizar su respuesta para futuros lectores?
Cyril Duchon-Doris
Estoy recibiendoundefined method 'pluck' for #<Array:...>
Wylliam Judd
7

¡La solución de Steven Soroka es realmente genial! No tengo la reputación de comentar una respuesta (por eso estoy agregando una nueva respuesta: P) pero creo que usar el mapa para una relación es costoso (especialmente si su relación has_many tiene hunders | miles de registros) porque se pone los datos de la base de datos, construye cada registro, genera la matriz original y luego itera sobre la matriz original para construir una nueva con los valores del bloque dado.

Usar pluck es más rápido y quizás la opción más rápida.

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

Aquí algunas estadísticas con 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> 

Estoy usando solo 250 citas. ¡No olvide agregar índices a: patient_id y: doctor_id en el documento de cita!

Espero que te ayude, ¡Gracias por leer!

franciscodelgadodev
fuente
Estoy recibiendoundefined method 'pluck' for #<Array:...>
Wylliam Judd
0

Quiero responder a esta pregunta desde la perspectiva de la asociación autorreferencial, no solo de has_many: a través de la perspectiva.

Digamos que tenemos un CRM con contactos. Los contactos tendrán relaciones con otros contactos, pero en lugar de crear una relación entre dos modelos diferentes, crearemos una relación entre dos instancias del mismo modelo. Un contacto puede tener muchos amigos y ser amigo de muchos otros contactos, por lo que tendremos que crear una relación de muchos a muchos.

Si usamos un RDBMS y ActiveRecord, usaríamos has_many: through. Por lo tanto, necesitaríamos crear un modelo de unión, como Friendship. Este modelo tendría dos campos, un contact_id que representa el contacto actual que está agregando un amigo y un friend_id que representa al usuario que se hace amigo.

Pero estamos usando MongoDB y Mongoid. Como se indicó anteriormente, Mongoid no tiene has_many: through o una función equivalente. No sería tan útil con MongoDB porque no admite consultas de combinación. Por lo tanto, para modelar una relación muchos-muchos en una base de datos que no es RDBMS como MongoDB, usa un campo que contiene una matriz de claves 'externas' en cada lado.

class Contact
  include Mongoid::Document
  has_and_belongs_to_many :practices
end

class Practice
  include Mongoid::Document
  has_and_belongs_to_many :contacts
end

Como dice la documentación:

Las relaciones de muchas a muchas en las que los documentos inversos se almacenan en una colección separada del documento base se definen utilizando la macro has_and_belongs_to_many de Mongoid. Esto exhibe un comportamiento similar al de Active Record con la excepción de que no se necesita una colección de combinaciones, los identificadores de claves foráneas se almacenan como matrices a ambos lados de la relación.

Al definir una relación de esta naturaleza, cada documento se almacena en su colección respectiva, y cada documento contiene una referencia de "clave externa" al otro en forma de matriz.

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

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

Ahora, para una Asociación de autorreferencia en MongoDB, tiene algunas opciones.

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

¿Cuál es la diferencia entre contactos relacionados y contactos que tienen muchos y pertenecen a muchas prácticas? ¡Gran diferencia! Uno es una relación entre dos entidades. Otro es una autorreferencia.

Donato
fuente
¿Los documentos de ejemplo parecen ser los mismos?
CyberMew