Como implementar has_many: por meio de relacionamentos com Mongoid e mongodb?

96

Usando este exemplo modificado dos guias Rails , como alguém modela uma associação relacional "has_many: through" usando mongoid?

O desafio é que mongoid não suporta has_many: through como 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
fonte

Respostas:

151

O Mongoid não tem has_many: through ou um recurso equivalente. Não seria tão útil com o MongoDB porque não oferece suporte a consultas de junção, portanto, mesmo que você pudesse fazer referência a uma coleção relacionada por meio de outra, ainda assim exigiria várias consultas.

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

Normalmente, se você tiver um relacionamento muitos-muitos em um RDBMS, você modelaria isso de forma diferente no MongoDB usando um campo contendo um array de chaves 'estrangeiras' em cada lado. Por exemplo:

class Physician
  include Mongoid::Document
  has_and_belongs_to_many :patients
end

class Patient
  include Mongoid::Document
  has_and_belongs_to_many :physicians
end

Em outras palavras, você eliminaria a tabela de junção e teria um efeito semelhante a has_many: through em termos de acesso ao 'outro lado'. Mas, no seu caso, isso provavelmente não é apropriado porque sua tabela de junção é uma classe Appointment que carrega algumas informações extras, não apenas a associação.

Como você modela isso depende, em certa medida, das consultas que você precisa executar, mas parece que você precisará adicionar o modelo de consulta e definir associações para paciente e médico da seguinte forma:

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

Com relacionamentos no MongoDB, você sempre precisa fazer uma escolha entre documentos incorporados ou associados. Em seu modelo, acho que MeetingNotes é um bom candidato para um relacionamento integrado.

class Appointment
  include Mongoid::Document
  embeds_many :meeting_notes
end

class MeetingNote
  include Mongoid::Document
  embedded_in :appointment
end

Isso significa que você pode recuperar as notas junto com um compromisso todos juntos, enquanto você precisaria de várias consultas se isso fosse uma associação. Você apenas tem que ter em mente o limite de tamanho de 16 MB para um único documento que pode entrar em jogo se você tiver um grande número de anotações de reunião.

Steve
fonte
7
1 resposta muito boa, apenas para informação, o limite de tamanho do mongodb foi aumentado para 16 MB.
rubish
1
Por curiosidade (desculpe pela consulta atrasada), também sou novo no Mongoid e gostaria de saber como você consultaria dados quando se trata de um relacionamento nn usando uma coleção separada para armazenar a associação, é o mesmo que era com ActiveRecord?
innospark
38

Apenas para expandir isso, aqui estão os modelos estendidos com métodos que agem de maneira muito semelhante ao has_many: por meio de ActiveRecord retornando um proxy de consulta em vez de uma 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
fonte
2
isso certamente ajudou a fazer com que meu método de recuperação retornasse um array que bagunçava a paginação.
prasad.surase
1
Sem mágica. @CyrilDD, do que você está se referindo? map (&: doctor_id) é uma abreviação de map {| nomeação | nomeação.physician.id}
Steven Soroka
Eu me pergunto: essa abordagem reduz a frustração potencial com o limite de tamanho de documento de 16 MB, visto que os documentos não são incorporados, mas associados usando um modelo externo? (desculpe se esta é uma pergunta iniciante!)
Attila Györffy
Como Francis explica, usar .pluck()sin em vez de .mapé MUITO mais rápido. Você pode atualizar sua resposta para futuros leitores?
Cyril Duchon-Doris
Estou recebendoundefined method 'pluck' for #<Array:...>
Wylliam Judd
7

A solução Steven Soroka é realmente ótima! Não tenho a reputação de comentar uma resposta (é por isso que estou adicionando uma nova resposta: P), mas acho que usar o mapa para um relacionamento é caro (especialmente se o seu relacionamento has_many tiver centenas | milhares de registros) porque ele fica os dados do banco de dados, constroem cada registro, gera o array original e então itera sobre o array original para construir um novo com os valores do bloco dado.

Usar depenar é mais rápido e talvez a opção mais 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

Aqui estão algumas estatísticas com 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> 

Estou usando apenas 250 compromissos. Não se esqueça de adicionar índices a: patient_id e: doctor_id no documento de compromisso!

Espero que ajude, obrigado pela leitura!

franciscodelgadodev
fonte
Estou recebendoundefined method 'pluck' for #<Array:...>
Wylliam Judd
0

Eu quero responder a essa questão da perspectiva da associação autorreferencial, não apenas da perspectiva has_many: por meio da perspectiva.

Digamos que temos um CRM com contatos. Os contatos terão relacionamentos com outros contatos, mas em vez de criar um relacionamento entre dois modelos diferentes, estaremos criando um relacionamento entre duas instâncias do mesmo modelo. Um contato pode ter muitos amigos e ser amigo de muitos outros contatos, então teremos que criar um relacionamento muitos para muitos.

Se estivermos usando um RDBMS e ActiveRecord, usaríamos has_many: through. Portanto, precisaríamos criar um modelo de associação, como Amizade. Este modelo teria dois campos, um contact_id que representa o contato atual que está adicionando um amigo e um friend_id que representa o usuário do qual está fazendo amizade.

Mas estamos usando MongoDB e Mongoid. Conforme declarado acima, o Mongoid não tem has_many: through ou um recurso equivalente. Não seria tão útil com o MongoDB porque não oferece suporte a consultas de junção. Portanto, para modelar um relacionamento muitos-muitos em um banco de dados não-RDBMS como o MongoDB, você usa um campo que contém uma matriz de chaves 'estrangeiras' em 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

Conforme afirma a documentação:

Muitos para muitos relacionamentos em que os documentos inversos são armazenados em uma coleção separada do documento base são definidos usando a macro has_and_belongs_to_many do Mongoid. Isso exibe um comportamento semelhante ao Active Record, com a exceção de que nenhuma coleção de junção é necessária, os IDs de chave estrangeira são armazenados como matrizes em ambos os lados da relação.

Ao definir uma relação desta natureza, cada documento é armazenado em sua respectiva coleção, e cada documento contém uma referência de “chave estrangeira” para o outro em forma de array.

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

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

Agora, para uma associação de autorreferência no MongoDB, você tem algumas opções.

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

Qual é a diferença entre contatos relacionados e contatos que têm muitos e pertencem a muitas clínicas? Enorme diferença! Um é um relacionamento entre duas entidades. Outro é uma auto-referência.

Donato
fonte
Os documentos de exemplo parecem ser os mesmos?
CyberMew