Jak zaimplementować has_many: poprzez relacje z Mongoidem i Mongodb?

96

Korzystając z tego zmodyfikowanego przykładu z przewodników Railsów , w jaki sposób można modelować relacyjne powiązanie „has_many:” za pomocą „mongoid”?

Wyzwanie polega na tym, że mongoid nie obsługuje has_many: through, tak jak robi to 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
źródło

Odpowiedzi:

151

Mongoid nie ma funkcji has_many: through ani równoważnej funkcji. Nie byłoby to tak przydatne w MongoDB, ponieważ nie obsługuje zapytań łączących, więc nawet gdybyś mógł odwoływać się do powiązanej kolekcji za pośrednictwem innej, nadal wymagałoby to wielu zapytań.

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

Zwykle, jeśli masz relację wiele-wiele w RDBMS, możesz modelować to inaczej w MongoDB, używając pola zawierającego tablicę kluczy „obcych” po obu stronach. Na przykład:

class Physician
  include Mongoid::Document
  has_and_belongs_to_many :patients
end

class Patient
  include Mongoid::Document
  has_and_belongs_to_many :physicians
end

Innymi słowy, wyeliminowałbyś tabelę łączenia i miałoby to podobny efekt do has_many: poprzez dostęp do „drugiej strony”. Ale w twoim przypadku prawdopodobnie nie jest to właściwe, ponieważ twój stół do łączenia jest klasą spotkań, która zawiera dodatkowe informacje, a nie tylko skojarzenie.

Sposób, w jaki to modelujesz, zależy w pewnym stopniu od zapytań, które musisz uruchomić, ale wydaje się, że będziesz musiał dodać model spotkania i zdefiniować powiązania z pacjentem i lekarzem, coś takiego:

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

W przypadku relacji w MongoDB zawsze musisz wybierać między osadzonymi lub powiązanymi dokumentami. Domyślam się, że w Twoim modelu MeetingNotes jest dobrym kandydatem do osadzenia relacji.

class Appointment
  include Mongoid::Document
  embeds_many :meeting_notes
end

class MeetingNote
  include Mongoid::Document
  embedded_in :appointment
end

Oznacza to, że możesz razem pobrać notatki razem ze spotkaniem, podczas gdy w przypadku skojarzenia potrzebnych byłoby wiele zapytań. Musisz tylko pamiętać o ograniczeniu rozmiaru do 16 MB dla pojedynczego dokumentu, który może się pojawić, jeśli masz bardzo dużą liczbę notatek ze spotkania.

Steve
źródło
7
+1 bardzo dobra odpowiedź, tylko dla informacji, limit rozmiaru mongodb został zwiększony do 16 MB.
rubish
1
Z ciekawości (przepraszam za spóźnione zapytanie), jestem też nowy w Mongoid i zastanawiałem się, jak można wyszukiwać dane, gdy jest to relacja nn, używając oddzielnej kolekcji do przechowywania skojarzenia, czy to jest to samo, co było z ActiveRecord?
innospark
38

Aby to rozwinąć, oto modele rozszerzone o metody, które działają bardzo podobnie do has_many: poprzez z ActiveRecord, zwracając proxy zapytania zamiast tablicy rekordów:

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
źródło
2
to z pewnością pomogło, ponieważ moją metodą pobierania było zwrócenie tablicy, która zepsuła paginację.
prasad.surase
1
Żadnej magii. @CyrilDD, o czym mówisz? mapa (&: physician_id) to krótka ręka dla mapy {| nominacja | mianowanie.physician.id}
Steven Soroka
Zastanawiam się, czy takie podejście zmniejsza potencjalną frustrację związaną z limitem rozmiaru dokumentu wynoszącym 16 MB, biorąc pod uwagę, że dokumenty nie są osadzone, ale zamiast tego powiązane z użyciem zewnętrznego modelu? (przepraszam, jeśli to pytanie noobowe!)
Attila Györffy
Jak wyjaśnia Francis, używanie .pluck()sinstead .mapjest DUŻO szybsze. Czy możesz zaktualizować swoją odpowiedź dla przyszłych czytelników?
Cyril Duchon-Doris
Dostajęundefined method 'pluck' for #<Array:...>
Wylliam Judd
7

Rozwiązanie Stevena Soroki jest naprawdę świetne! Nie mam reputacji, by komentować odpowiedź (dlatego dodaję nową odpowiedź: P), ale myślę, że używanie mapy do związku jest kosztowne (szczególnie jeśli twój związek has_many ma setki | tysiące rekordów), ponieważ dane z bazy danych, buduj każdy rekord, generuje oryginalną tablicę, a następnie wykonuje iterację po oryginalnej tablicy, aby zbudować nową z wartościami z danego bloku.

Używanie skubania jest szybsze i być może najszybszą opcją.

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

Oto kilka statystyk z 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> 

Korzystam tylko z 250 spotkań. Nie zapomnij dodać indeksów do: patient_id i: physician_id w dokumencie spotkania!

Mam nadzieję, że to pomoże. Dziękuję za przeczytanie!

franciscodelgadodev
źródło
Dostajęundefined method 'pluck' for #<Array:...>
Wylliam Judd
0

Chcę odpowiedzieć na to pytanie z perspektywy asocjacji odnoszących się do samych siebie, a nie tylko z perspektywy has_many: poprzez perspektywę.

Powiedzmy, że mamy CRM z kontaktami. Kontakty będą miały relacje z innymi kontaktami, ale zamiast tworzyć relacje między dwoma różnymi modelami, utworzymy relację między dwoma wystąpieniami tego samego modelu. Kontakt może mieć wielu przyjaciół i być zaprzyjaźniony z wieloma innymi kontaktami, więc będziemy musieli stworzyć relację wiele do wielu.

Jeśli używamy RDBMS i ActiveRecord, użylibyśmy has_many: through. Dlatego musielibyśmy stworzyć model łączenia, taki jak Przyjaźń. Ten model miałby dwa pola, contact_id, które reprezentuje bieżący kontakt, który dodaje znajomego, oraz friend_id, który reprezentuje zaprzyjaźnionego użytkownika.

Ale używamy MongoDB i Mongoid. Jak wspomniano powyżej, Mongoid nie ma funkcji has_many: through ani równoważnej funkcji. Nie byłoby to tak przydatne w MongoDB, ponieważ nie obsługuje zapytań łączących. Dlatego w celu modelowania relacji wiele-wiele w bazie danych innej niż RDBMS, takiej jak MongoDB, należy użyć pola zawierającego tablicę kluczy „obcych” po obu stronach.

class Contact
  include Mongoid::Document
  has_and_belongs_to_many :practices
end

class Practice
  include Mongoid::Document
  has_and_belongs_to_many :contacts
end

Jak podaje dokumentacja:

Wiele do wielu relacji, w których odwrotne dokumenty są przechowywane w oddzielnej kolekcji od dokumentu podstawowego, jest definiowanych za pomocą makra has_and_belongs_to_many Mongoida. Zachowuje się to podobnie jak w module Active Record, z wyjątkiem tego, że nie jest potrzebna żadna kolekcja złączeń, identyfikatory kluczy obcych są przechowywane jako tablice po obu stronach relacji.

Podczas definiowania relacji tego rodzaju, każdy dokument jest przechowywany w swojej odpowiedniej kolekcji, a każdy dokument zawiera odniesienie do innego „klucza obcego” w postaci tablicy.

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

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

Teraz, jeśli chodzi o stowarzyszenie odwołujące się do siebie w MongoDB, masz kilka opcji.

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

Jaka jest różnica między kontaktami pokrewnymi a kontaktami, które mają wiele i należą do wielu praktyk? Duża różnica! Jedna to relacja między dwoma podmiotami. Inny to odniesienie do samego siebie.

Donato
źródło
Przykładowe dokumenty wydają się być takie same?
CyberMew