Comment implémenter has_many: à travers des relations avec Mongoid et mongodb?

96

En utilisant cet exemple modifié des guides Rails , comment modéliser une association relationnelle "has_many: through" en utilisant mongoid?

Le défi est que mongoid ne prend pas en charge has_many: through comme le fait 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
la source

Réponses:

151

Mongoid n'a pas has_many: through ou une fonctionnalité équivalente. Cela ne serait pas si utile avec MongoDB car il ne prend pas en charge les requêtes de jointure, donc même si vous pouviez référencer une collection associée via une autre, cela nécessiterait toujours plusieurs requêtes.

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

Normalement, si vous avez une relation plusieurs-plusieurs dans un SGBDR, vous modéliseriez cela différemment dans MongoDB en utilisant un champ contenant un tableau de clés «étrangères» de chaque côté. Par exemple:

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 d'autres termes, vous élimineriez la table de jointure et cela aurait un effet similaire à has_many: through en termes d'accès à «l'autre côté». Mais dans votre cas, ce n'est probablement pas approprié car votre table de jointure est une classe de rendez-vous qui contient des informations supplémentaires, pas seulement l'association.

La façon dont vous modélisez cela dépend dans une certaine mesure des requêtes que vous devez exécuter, mais il semble que vous deviez ajouter le modèle de rendez-vous et définir des associations entre le patient et le médecin, comme ceci:

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

Avec les relations dans MongoDB, vous devez toujours faire un choix entre des documents intégrés ou associés. Dans votre modèle, je suppose que MeetingNotes est un bon candidat pour une relation intégrée.

class Appointment
  include Mongoid::Document
  embeds_many :meeting_notes
end

class MeetingNote
  include Mongoid::Document
  embedded_in :appointment
end

Cela signifie que vous pouvez récupérer les notes avec un rendez-vous tous ensemble, alors que vous auriez besoin de plusieurs requêtes s'il s'agissait d'une association. Il suffit de garder à l'esprit la taille limite de 16 Mo pour un seul document qui pourrait entrer en jeu si vous avez un très grand nombre de notes de réunion.

Steve
la source
7
+1 très belle réponse, juste pour info, la limite de taille de mongodb a été augmentée à 16 Mo.
rubish
1
Par curiosité (désolé pour l'enquête tardive), je suis également nouveau sur Mongoid et je me demandais comment vous interrogeriez des données lorsqu'il s'agit d'une relation nn en utilisant une collection distincte pour stocker l'association, est-ce la même chose avec ActiveRecord?
innospark
38

Juste pour développer cela, voici les modèles étendus avec des méthodes qui agissent très similaires à has_many: through d'ActiveRecord en renvoyant un proxy de requête au lieu d'un tableau d'enregistrements:

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
la source
2
cela a sûrement aidé parce que ma méthode de récupération renvoyait un tableau qui perturbait la pagination.
prasad.surase
1
Pas de magie. @CyrilDD, de quoi parlez-vous? map (&: doctor_id) est un raccourci pour map {| rendez-vous | nomination.physician.id}
Steven Soroka
Je me demande, cette approche réduit-elle la frustration potentielle avec la limite de taille de document de 16 Mo, étant donné que les documents ne sont pas incorporés mais plutôt associés à l'aide d'un modèle extérieur? (désolé si c'est une question noob!)
Attila Györffy
Comme l'explique Francis, utiliser le .pluck()péché au lieu de .mapest BIEN PLUS rapide. Pouvez-vous mettre à jour votre réponse pour les futurs lecteurs?
Cyril Duchon-Doris
Je reçoisundefined method 'pluck' for #<Array:...>
Wylliam Judd
7

La solution Steven Soroka est vraiment géniale! Je n'ai pas la réputation de commenter une réponse (c'est pourquoi j'ajoute une nouvelle réponse: P) mais je pense que l'utilisation de map pour une relation coûte cher (surtout si votre relation has_many a des centaines | des milliers d'enregistrements) les données de la base de données, construisez chaque enregistrement, génère le tableau d'origine, puis itère sur le tableau d'origine pour en créer un nouveau avec les valeurs du bloc donné.

L'utilisation de la pince est plus rapide et peut-être l'option la plus rapide.

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

Voici quelques statistiques avec 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> 

J'utilise seulement 250 rendez-vous. N'oubliez pas d'ajouter des index à: patient_id et: doctor_id dans le document de rendez-vous!

J'espère que cela aide, merci d'avoir lu!

franciscodelgadodev
la source
Je reçoisundefined method 'pluck' for #<Array:...>
Wylliam Judd
0

Je veux répondre à cette question du point de vue de l'association auto-référencée, pas seulement du has_many: through perspective.

Disons que nous avons un CRM avec des contacts. Les contacts auront des relations avec d'autres contacts, mais au lieu de créer une relation entre deux modèles différents, nous créerons une relation entre deux instances du même modèle. Un contact peut avoir de nombreux amis et se lier d'amitié avec de nombreux autres contacts, nous allons donc devoir créer une relation plusieurs-à-plusieurs.

Si nous utilisons un SGBDR et ActiveRecord, nous utiliserions has_many: through. Ainsi, nous aurions besoin de créer un modèle de jointure, comme l'amitié. Ce modèle aurait deux champs, un contact_id qui représente le contact actuel qui ajoute un ami et un friend_id qui représente l'utilisateur qui se lie d'amitié.

Mais nous utilisons MongoDB et Mongoid. Comme indiqué ci-dessus, Mongoid n'a pas has_many: through ou une fonctionnalité équivalente. Cela ne serait pas si utile avec MongoDB car il ne prend pas en charge les requêtes de jointure. Par conséquent, afin de modéliser une relation plusieurs-plusieurs dans une base de données non SGBDR comme MongoDB, vous utilisez un champ contenant un tableau de clés «étrangères» de chaque côté.

class Contact
  include Mongoid::Document
  has_and_belongs_to_many :practices
end

class Practice
  include Mongoid::Document
  has_and_belongs_to_many :contacts
end

Comme l'indique la documentation:

Les relations plusieurs à plusieurs dans lesquelles les documents inverses sont stockés dans une collection distincte du document de base sont définies à l'aide de la macro has_and_belongs_to_many de Mongoid. Cela présente un comportement similaire à Active Record à l'exception qu'aucune collection de jointure n'est nécessaire, les ID de clé étrangère sont stockés sous forme de tableaux de chaque côté de la relation.

Lors de la définition d'une relation de cette nature, chaque document est stocké dans sa collection respective, et chaque document contient une référence «clé étrangère» à l'autre sous la forme d'un tableau.

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

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

Maintenant, pour une association d'auto-référencement dans MongoDB, vous avez quelques options.

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

Quelle est la différence entre des contacts liés et des contacts ayant plusieurs et appartenant à de nombreuses pratiques? Énorme différence! L'une est une relation entre deux entités. L'autre est une auto-référence.

Donato
la source
Les exemples de documents semblent être les mêmes?
CyberMew