Associações
Nesta página
- Associações referenciadas
- Tem um
- Tem muitos
- Pertence a
- Tem e pertence a muitos
- Consultando associações referenciadas
- Associações incorporadas
- Incorpora um
- Incorpora muitos
- Incorporação recursiva
- Referência versus incorporação
- Consulta de associações incorporadas
- Omissão de
_id
campos - Excluindo
- Atribuição de hash
- Comportamento comum
- Extensões
- Nomes de associação personalizados
- Chaves primárias e estrangeiras personalizadas
- Escopos personalizados
- Validações
- Polimorfismo
- Retornos de chamada em cascata
- Comportamento dependente
- Salvamento automático
- Predicados de existência
- Construção automática
- Tocante
- A opção counter_cache
- Proxies de associação
- Metadados de associação
- Atributos
- O objeto de associação
Associações referenciadas
O Mongoid suporta as associações has_one
, has_many
, belongs_to
e has_and_belongs_to_many
familiares aos usuários do ActiveRecord.
Tem um
Use a macro has_one
para declarar que o principal tem um secundário armazenado em uma collection separada. Por padrão, o secundário é opcional:
class Band include Mongoid::Document has_one :studio end
Ao utilizar o has_one
, o modelo filho deve utilizar o belongs_to
para declarar a associação com o pai:
class Studio include Mongoid::Document belongs_to :band end
Dadas as definições acima, cada documento filho contém uma referência ao respectivo documento pai:
band = Band.create!(studio: Studio.new) # => #<Band _id: 600114fa48966848ad5bd392, > band.studio # => #<Studio _id: 600114fa48966848ad5bd391, band_id: BSON::ObjectId('600114fa48966848ad5bd392')>
Use validações para exigir que a criança esteja presente:
class Band include Mongoid::Document has_one :studio validates_presence_of :studio end
Tem muitos
Utilize a associação has_many
para declarar que o pai tem zero ou mais filhos armazenados em uma collection separada:
class Band include Mongoid::Document has_many :members end
Como no has_one
, o modelo filho deve usar belongs_to
para declarar a associação com o pai:
class Member include Mongoid::Document belongs_to :band end
Além disso, como acontece com has_one
, os documentos da criança contêm referências aos respectivos pais:
band = Band.create!(members: [Member.new]) # => #<Band _id: 6001166d4896684910b8d1c5, > band.members # => [#<Member _id: 6001166d4896684910b8d1c6, band_id: BSON::ObjectId('6001166d4896684910b8d1c5')>]
Use validações para exigir que pelo menos uma criança esteja presente:
class Band include Mongoid::Document has_many :members validates_presence_of :members end
Consultas
any?
Use o método any?
na associação para determinar com eficiência se a associação contém algum documento, sem recuperar todo o conjunto de documents do banco de dados:
band = Band.first band.members.any?
any?
implementa também a Enumerable#any? API, permitindo a filtragem com um bloco:
band = Band.first band.members.any? { |member| member.instrument == 'piano' }
... ou por um nome de classe que pode ser útil para associações polimórficas:
class Drummer < Member end band = Band.first band.members.any?(Drummer)
Se a associação já estiver carregada, any?
inspecionará os documentos carregados e não consultará o banco de dados:
band = Band.first # Queries the database band.members.any? band.members.to_a # Does not query the database band.members.any?
Observe que simplesmente chamar any?
não carregaria a associação (já que any?
recupera apenas o campo _id do primeiro documento correspondente).
exists?
O método exists?
na associação determina se há algum documento persistente na associação. Ao contrário do método any?
:
exists?
sempre consulta o banco de dados, mesmo que a associação já esteja carregada.exists?
não considera documentos não persistentes.exists?
não permite a filtragem no aplicativo, como fazany?
, e não aceita nenhum argumento.
O exemplo seguinte ilustra a diferença entre exists?
e any?
:
band = Band.create! # Member is not persisted. band.members.build band.members.any? # => true band.members.exists? # => false # Persist the member. band.members.map(&:save!) band.members.any? # => true band.members.exists? # => true
Pertence a
Use a macro belongs_to
para associar um filho a um pai armazenado em uma coleção separada. O _id
do pai (se um pai estiver associado) é armazenado no filho.
Por padrão, se uma associação belongs_to
for definida em um modelo, é necessário fornecer um valor para que uma instância do modelo possa ser salva. Use a opção optional: true`
para tornar as instâncias persistentes sem especificar o principal:
class Band include Mongoid::Document has_one :studio end class Studio include Mongoid::Document belongs_to :band, optional: true end studio = Studio.create! # => #<Studio _id: 600118184896684987aa884f, band_id: nil>
Para alterar o comportamento padrão das associações do belongs_to
para não exigir seus respectivos pais globalmente, configure aopção de configuração do belongs_to_required_by_default
para false
.
Embora as associações has_one
e has_many
exijam que a associação belongs_to
correspondente seja definida no filho, o belongs_to
também pode ser utilizado sem uma macro has_one
ou has_many
correspondente. Nesse caso, o filho não está acessível a partir do pai, mas o pai está acessível a partir do filho:
class Band include Mongoid::Document end class Studio include Mongoid::Document belongs_to :band end
Para maior clareza, é possível adicionar a opção inverse_of: nil
nos casos em que o pai não define a associação:
class Band include Mongoid::Document end class Studio include Mongoid::Document belongs_to :band, inverse_of: nil end
Tem e pertence a muitos
Use a macro has_and_belongs_to_many
para declarar uma associação de muitos para muitos:
class Band include Mongoid::Document has_and_belongs_to_many :tags end class Tag include Mongoid::Document has_and_belongs_to_many :bands end
Ambas as instâncias de modelo armazenam uma lista de IDs dos modelos associados, se houver:
band = Band.create!(tags: [Tag.create!]) # => #<Band _id: 60011d554896684b8b910a2a, tag_ids: [BSON::ObjectId('60011d554896684b8b910a29')]> band.tags # => [#<Tag _id: 60011d554896684b8b910a29, band_ids: [BSON::ObjectId('60011d554896684b8b910a2a')]>]
Você pode criar uma associação has_and_belongs_to_many
unilateral para armazenar as IDs somente em um documento usando a opção inverse_of: nil
:
class Band include Mongoid::Document has_and_belongs_to_many :tags, inverse_of: nil end class Tag include Mongoid::Document end band = Band.create!(tags: [Tag.create!]) # => #<Band _id: 60011dbc4896684bbbaa9255, tag_ids: [BSON::ObjectId('60011dbc4896684bbbaa9254')]> band.tags # => [#<Tag _id: 60011dbc4896684bbbaa9254, >]
Uma associação has_and_belongs_to_many
unilateral, naturalmente, só pode ser usada a partir do modelo em que está definida.
Observação
Dado dois modelos, A e B, onde A has_and_belongs_to_many
B, ao adicionar um documento do tipo B à associação HABTM em um documento do tipo A, o Mongoid não atualizará o campo updated_at
para o documento do tipo A, mas atualizará o campo updated_at
para o documento do tipo B.
Consultando associações referenciadas
Na maioria dos casos, consultas eficientes em associações referenciadas (e, em geral, envolvendo dados, condições ou várias coleções) são realizadas usando o pipeline de agregação. Ajudantes do Mongoid para construir consultas de pipeline de agregação são descritos na seção de pipeline de agregação.
Para consultas simples, o uso do pipeline de agregação pode ser evitado e as associações podem ser consultadas diretamente. Ao consultar associações diretamente, todas as condições devem estar apenas na coleção dessa associação (o que normalmente significa associação em questão e quaisquer associações embutidas nela).
Por exemplo, dado os modelos a seguir:
class Band include Mongoid::Document has_many :tours has_many :awards field :name, type: String end class Tour include Mongoid::Document belongs_to :band field :year, type: Integer end class Award include Mongoid::Document belongs_to :band field :name, type: String end
Pode-se recuperar todas as bandas que fizeram turnês desde 2000 da seguinte forma:
band_ids = Tour.where(year: {'$gte' => 2000}).pluck(:band_id) bands = Band.find(band_ids)
As condições em Tour
podem ser arbitrariamente complexas, mas todas devem estar no mesmo documento Tour
(ou documentos incorporados em Tour
).
Para encontrar prêmios para bandas que fizeram turnês desde 2000:
band_ids = Tour.where(year: {'$gte' => 2000}).pluck(:band_id) awards = Award.where(band_id: {'$in' => band_ids})
Associações incorporadas
Graças ao modelo de documentos do MongoDB, o Mongoid também oferece associações incorporadas que permitem que documentos de diferentes tipos sejam armazenados hierarquicamente na mesma coleção. Associações incorporadas são definidas usando macros embeds_one
, embeds_many
e embedded_in
, além de recursively_embeds_one
e recursively_embeds_many
para incorporação recursiva.
Incorpora um
As associações um a um em que os filhos são incorporados ao documento pai são definidas usando as macros embeds_one
e embedded_in
do Mongoid.
Definição
O documento pai da associação deve usar a macro embeds_one
para indicar que tem um filho incorporado, onde o documento incorporado usa embedded_in
. São necessárias definições em ambos os lados da associação para que ela funcione corretamente.
class Band include Mongoid::Document embeds_one :label end class Label include Mongoid::Document field :name, type: String embedded_in :band end
Armazenamento
Os documentos incorporados usando a macro embeds_one
são armazenados como um hash dentro do pai na coleção de banco de dados do pai.
{ "_id" : ObjectId("4d3ed089fb60ab534684b7e9"), "label" : { "_id" : ObjectId("4d3ed089fb60ab534684b7e0"), "name" : "Mute", } }
Como opção, você pode instruir o Mongoid a armazenar o documento incorporado em um atributo diferente do nome, basta fornecer a opção :store_as
.
class Band include Mongoid::Document embeds_one :label, store_as: "lab" end
Incorpora muitos
Relacionamentos de um a muitos em que os filhos estão incorporados no documento principal são definidos usando as macros embeds_many
e embedded_in
do Mongoid.
Definição
O documento principal da associação deve usar a macro embeds_many
para indicar que tem muitos filhos incorporados, enquanto o documento incorporado usa embedded_in
. As definições são necessárias em ambos os lados da associação para que ela funcione corretamente.
class Band include Mongoid::Document embeds_many :albums end class Album include Mongoid::Document field :name, type: String embedded_in :band end
Armazenamento
Documentos incorporados usando a macro embeds_many
são armazenados como uma matriz de hashes dentro do pai na coleção de banco de dados do pai.
{ "_id" : ObjectId("4d3ed089fb60ab534684b7e9"), "albums" : [ { "_id" : ObjectId("4d3ed089fb60ab534684b7e0"), "name" : "Violator", } ] }
Como opção, você pode instruir o Mongoid a armazenar o documento incorporado em um atributo diferente do nome, basta fornecer a opção :store_as
.
class Band include Mongoid::Document embeds_many :albums, store_as: "albs" end
Incorporação recursiva
Um documento pode ser incorporado recursivamente usando recursively_embeds_one
ou recursively_embeds_many
, que fornece acessadores para o pai e os filhos por meio dos métodos parent_
e child_
.
class Tag include Mongoid::Document field :name, type: String recursively_embeds_many end root = Tag.new(name: "programming") child_one = root.child_tags.build child_two = root.child_tags.build root.child_tags # [ child_one, child_two ] child_one.parent_tag # [ root ] child_two.parent_tag # [ root ] class Node include Mongoid::Document recursively_embeds_one end root = Node.new child = Node.new root.child_node = child root.child_node # child child.parent_node # root
Referência versus incorporação
Embora uma discussão completa sobre referência versus incorporação esteja além do escopo deste tutorial, aqui estão algumas considerações de alto nível para a escolha de um em detrimento do outro.
Quando uma associação é incorporada, os documentos pai e filho são armazenados na mesma coleção. Isso permite a persistência e a recuperação eficientes quando ambos são usados/necessários. Por exemplo, se a barra de navegação de um site mostra atributos de um usuário que estão armazenados nos próprios documentos, geralmente é uma boa ideia usar associações incorporadas.
O uso de associações incorporadas permite o uso de ferramentas do MongoDB, como o pipeline de agregação, para consultar esses documentos de forma eficiente.
Como os documentos incorporados são armazenados como parte de seus documentos de nível superior pai, não é possível manter um documento incorporado por si só, nem é possível recuperar documentos incorporados diretamente. No entanto, os documentos incorporados ainda podem ser consultados e recuperados com eficiência com a ajuda da operação de projeção do MongoDB:
class Band include Mongoid::Document field :started_on, type: Date embeds_one :label end class Label include Mongoid::Document field :name, type: String embedded_in :band end # Retrieve labels for bands started in the last year. # # Sends a find query like this: # {"find"=>"bands", # "filter"=>{"started_on"=>{"$gt"=>2018-07-01 00:00:00 UTC}}, # "projection"=>{"_id"=>1, "label"=>1}} Band.where(started_on: {'$gt' => Time.now - 1.year}).only(:label).map(&:label).compact.uniq
Definindo valores obsoletos em associações referenciadas
Configurar um valor obsoleto para uma associação referenciada pode às vezes resultar em um valor de nil
sendo persistente no banco de dados. Veja o seguinte caso:
class Post include Mongoid::Document has_one :comment, inverse_of: :post end class Comment include Mongoid::Document belongs_to :post, inverse_of: :comment, optional: true end post.comment = comment1 post.reload
Neste ponto, post.comment
está definido como comment1
, no entanto, desde que uma recarga aconteceu, post.comment
não se refere ao mesmo objeto que comment1
. Ou seja, atualizar um objeto não atualiza implicitamente o outro. Isso é importante para a próxima operação:
post.comment = comment2 post.reload
Agora, post.comment
está definido como comment2
e o post_id
do comentário antigo está definido como nil
. No entanto, o valor atribuído a post.comment
não se referia ao mesmo objeto de comment1
, portanto, embora o valor antigo de post.comment
tenha sido atualizado para ter nil
post_id
, comment1
ainda tem o conjunto post_id
.
post.comment = comment1 post.reload
Finalmente, esta última tarefa tenta definir o post_id
em comment1
, que deve ser nil
neste ponto, mas está definida para o antigo post_id
. Durante esta operação, o post_id
é apagado de comment2
e o novo post_id
é configurado em comment1
. No entanto, como a post_id
já estava definida em comment1
, nada persiste, e acabamos com ambos os comentários tendo um nil
post_id
. Neste ponto, executar post.comment
retorna nil
.
Consulta de associações incorporadas
Ao fazer query de documentos de nível superior, as condições podem ser especificadas em documentos em associações incorporadas usando a notação de ponto. Por exemplo, dado os modelos a seguir:
class Band include Mongoid::Document embeds_many :tours embeds_many :awards field :name, type: String end class Tour include Mongoid::Document embedded_in :band field :year, type: Integer end class Award include Mongoid::Document embedded_in :band field :name, type: String end
Para recuperar bandas com base nos atributos do tour, use a notação de ponto da seguinte maneira:
# Get all bands that have toured since 2000 Band.where('tours.year' => {'$gte' => 2000})
Para recuperar apenas documentos de associações incorporadas, sem recuperar documentos de nível superior, use o método de projeção pluck
:
# Get awards for bands that have toured since 2000 Band.where('tours.year' => {'$gte' => 2000}).pluck(:awards)
Consultando associações carregadas
Os métodos de query do Mongoid podem ser usados em associações incorporadas de documentos que já estão carregados no aplicativo. Esse mecanismo é chamado de "correspondência incorporada" e é implementado inteiramente no Mongoid -- as queries NÃO são enviadas ao servidor.
Os seguintes operadores são compatíveis:
$regex (o campo
$options
só é suportado quando o argumento$regex
é uma string)
Por exemplo, ao usar as definições de modelo que acabamos de fornecer, poderíamos fazer query de passeios em uma banda carregada:
band = Band.where(name: 'Astral Projection').first tours = band.tours.where(year: {'$gte' => 2000})
Correspondência incorporada versus comportamento do servidor
A correspondência incorporada do Mongoid tem como objetivo oferecer suporte à mesma funcionalidade e semântica que as queries nativas na versão mais recente do servidor MongoDB. Observe as seguintes limitações conhecidas:
A correspondência embutida não é implementada para pesquisa de texto, operadores de consulta geoespacial, operadores que executam código JavaScript ($where) e operadores que são implementados por meio de outras funcionalidades do servidor , como $expr e $jsonSchema.
O Mongoid DSL expande os argumentos
Range
para hashes com condições$gte
e$lte
. Em alguns casos isso cria consultas falsas. Correspondentes incorporados elevam a exceçãoInvalidQuery
nesses casos. Os operadores conhecidos por serem afetados são$elemMatch
,$eq
,$gt
,$gte
,$lt
,$lte
e$ne
.Ao executar a correspondência embutida com
$regex
, não é atualmente possível especificar um objeto de expressão regular como o padrão e também fornecer opções.MongoDB Server 4.0 e servidores anteriores não validam argumentos
$type
estritamente (por exemplo, permitindo argumentos inválidos como 0). Isso é validado mais estritamente no lado do cliente.
Omissão de _id
campos
Por padrão, o Mongoid adiciona um campo _id
a cada documento incorporado. Isso permite fácil referência e operações nos documentos incorporados.
Esses campos _id
podem ser omitidos para economizar espaço de armazenamento. Para fazer isso, substitua a definição do campo _id nos documentos filho e remova o valor padrão:
class Order include Mongoid::Document embeds_many :line_items end class LineItem include Mongoid::Document embedded_in :order field :_id, type: Object end
Na versão atual do Mongoid, a definição de campo é obrigatória, mas sem um valor padrão especificado, nenhum valor será armazenado no banco de dados. Uma versão futura do Mongoid pode permitir a remoção de campos definidos anteriormente.
Observação
Remover o campo _id
significa que os documentos incorporados devem ser identificados por seus valores de atributo de conteúdo durante consultas, atualizações e exclusões.
Excluindo
O Mongoid oferece três métodos para excluir filhos de associações embeds_many
: clear
, destroy_all
e delete_all
.
clear
O método clear
utiliza o operador $unset para remover toda a associação do documento de host. Ele não executa destruir chamadas nos documentos que estão sendo removidos, agindo como delete_all
a este respeito:
band = Band.find(...) band.tours.clear
Se clear
for chamado em uma associação em um documento de host não salvo, ele ainda tentará remover a associação do banco de dados com base na _id
do documento do host:
band = Band.find(...) band.tours << Tour.new(...) unsaved_band = Band.new(id: band.id, tours: [Tour.new]) # Removes all tours from the persisted band due to _id match. unsaved_band.tours.clear band.tours # => []
delete_all
O método delete_all
remove os documentos que estão na associação utilizando o operador $pullAll. Ao contrário clear
, delete_all
:
Carrega a associação, se ainda não tiver sido carregada;
Remove apenas os documentos existentes no aplicativo.
delete_all
não executa destruir chamada de resposta nos documentos que estão sendo removidos.
Exemplo:
band = Band.find(...) band.tours.delete_all
destroy_all
O método delete_all
remove os documentos que estão na associação utilizando o operador $pullAll ao executar as chamadas destruídas. Como delete_all
, destroy_all
carrega a associação inteira se ela ainda não tiver sido carregada e remove apenas os documentos que existem no aplicação:
band = Band.find(...) band.tours.destroy_all
Atribuição de hash
Associações incorporadas permitem ao usuário atribuir um Hash
em vez de um documento a uma associação. Na atribuição, esse hash é forçado em um documento da classe da associação à qual está sendo atribuído. Veja o exemplo a seguir:
class Band include Mongoid::Document embeds_many :albums end class Album include Mongoid::Document field :name, type: String embedded_in :band end band = Band.create! band.albums = [ { name: "Narrow Stairs" }, { name: "Transatlanticism" } ] p band.albums # => [ #<Album _id: 633c71e93282a4357bb608e5, name: "Narrow Stairs">, #<Album _id: 633c71e93282a4357bb608e6, name: "Transatlanticism"> ]
Isso funciona para associações embeds_one
, embeds_many
e embedded_in
. Observe que você não pode atribuir hashes a associações referenciadas.
Comportamento comum
Extensões
Todas as associações podem ter extensões, que fornecem uma maneira de adicionar funcionalidade específica do aplicativo à associação. Elas são definidas fornecendo um bloco para a definição de associação.
class Person include Mongoid::Document embeds_many :addresses do def find_by_country(country) where(country: country).first end def chinese _target.select { |address| address.country == "China" } end end end person.addresses.find_by_country("Mongolia") # returns address person.addresses.chinese # returns [ address ]
Nomes de associação personalizados
Você pode nomear suas associações como quiser, mas se a classe não puder ser inferida pelo Mongoid a partir do nome, e nem o lado oposto, você deverá fornecer à macro algumas opções adicionais para informar ao Mongoid como conectá-las.
class Car include Mongoid::Document embeds_one :engine, class_name: "Motor", inverse_of: :machine end class Motor include Mongoid::Document embedded_in :machine, class_name: "Car", inverse_of: :engine end
Chaves primárias e estrangeiras personalizadas
Os campos usados ao procurar associações podem ser explicitamente especificados. O padrão é usar id
na associação "principal" e #{association_name}_id
na associação "secundário", por exemplo, com has_many/belongs_to:
class Company include Mongoid::Document has_many :emails end class Email include Mongoid::Document belongs_to :company end company = Company.find(id) # looks up emails where emails.company_id == company.id company.emails
Especifique um primary_key
diferente para alterar o nome do campo na associação "pai" e foreign_key
para alterar o nome do campo na associação "filho":
class Company include Mongoid::Document field :c, type: String has_many :emails, foreign_key: 'c_ref', primary_key: 'c' end class Email include Mongoid::Document # This definition of c_ref is automatically generated by Mongoid: # field :c_ref, type: Object # But the type can also be specified: field :c_ref, type: String belongs_to :company, foreign_key: 'c_ref', primary_key: 'c' end company = Company.find(id) # looks up emails where emails.c_ref == company.c company.emails
Com uma associação has_and_belongs_to_many, uma vez que os dados são armazenados em ambos os lados da associação, existem 4 campos configuráveis quando a associação é definida:
:primary_key
é o campo no modelo remoto que contém o valor pelo qual o modelo remoto é procurado.:foreign_key
é o campo no modelo local que armazena os valores:primary_key
.:inverse_primary_key
é o campo no modelo local que o modelo remoto usa para procurar os documentos do modelo local.:inverse_foreign_key
é o campo no modelo remoto armazenando os valores no:inverse_primary_key
.
Um exemplo pode deixar isso mais claro:
class Company include Mongoid::Document field :c_id, type: Integer field :e_ids, type: Array has_and_belongs_to_many :employees, primary_key: :e_id, foreign_key: :e_ids, inverse_primary_key: :c_id, inverse_foreign_key: :c_ids end class Employee include Mongoid::Document field :e_id, type: Integer field :c_ids, type: Array has_and_belongs_to_many :companies, primary_key: :c_id, foreign_key: :c_ids, inverse_primary_key: :e_id, inverse_foreign_key: :e_ids end company = Company.create!(c_id: 123) # => #<Company _id: 5c565ece026d7c461d8a9d4e, c_id: 123, e_ids: nil> employee = Employee.create!(e_id: 456) # => #<Employee _id: 5c565ee8026d7c461d8a9d4f, e_id: 456, c_ids: nil> company.employees << employee company # => #<Company _id: 5c565ece026d7c461d8a9d4e, c_id: 123, e_ids: [456]> employee # => #<Employee _id: 5c5883ce026d7c4b9e244c0c, e_id: 456, c_ids: [123]>
Observe que, assim como no campo #{association_name}_id
padrão, o Mongoid adiciona automaticamente um campo para a chave externa personalizada c_ref
ao modelo. No entanto, como o Mongoid não sabe que tipo de dados deve ser permitido no campo, o campo é criado com um tipo de objeto. Recomendamos definir explicitamente o campo com o tipo apropriado.
Escopos personalizados
Você pode definir um escopo específico em uma associação utilizando o parâmetro :scope
. O escopo é um filtro adicional que restringe quais objetos são considerados parte da associação — uma associação com escopo gerará apenas documentos que atendam à condição do escopo. O escopo pode ser:
Proc
com aridade zero ouum
Symbol
que referencia um escopo nomeado no modelo associado.
class Trainer has_many :pets, scope: -> { where(species: 'dog') } has_many :toys, scope: :rubber end class Pet belongs_to :trainer end class Toy scope :rubber, where(material: 'rubber') belongs_to :trainer end
Observação
É possível adicionar documentos que não satisfazem o escopo de uma associação a essa associação. Nesse caso, esses documentos aparecerão associados na memória e serão salvos no banco de dados, mas não estarão presentes quando a associação for consultada no futuro. Por exemplo:
trainer = Trainer.create! dog = Pet.create!(trainer: trainer, species: 'dog') cat = Pet.create!(trainer: trainer, species: 'cat') trainer.pets #=> [dog, cat] trainer.reload.pets #=> [dog]
Observação
A sintaxe do Mongoid para a associação de escopo difere da do ActiveRecord. O Mongoid usa o argumento de palavra-chave :scope
para consistência com outras opções de associação, enquanto no ActiveRecord o escopo é um argumento posicional.
Validações
É importante observar que, por padrão, o Mongoid validará os filhos de qualquer associação que seja carregada na memória por meio de um validates_associated
. As associações às quais isso se aplica são:
embeds_many
embeds_one
has_many
has_one
has_and_belongs_to_many
Se não quiser esse comportamento, você pode desativá-lo ao definir a associação.
class Person include Mongoid::Document embeds_many :addresses, validate: false has_many :posts, validate: false end
Polimorfismo
Associações de um para um e um para muitos suportam o polimorfismo, que é ter uma única associação potencialmente conter objetos de classes diferentes. Por exemplo, poderíamos modelar uma organização na qual departamentos e equipes têm gerentes da seguinte maneira:
class Department include Mongoid::Document has_one :manager, as: :unit end class Team include Mongoid::Document has_one :manager, as: :unit end class Manager include Mongoid::Document belongs_to :unit, polymorphic: true end dept = Department.create! team = Team.create! alice = Manager.create!(unit: dept) alice.unit == dept # => true dept.manager == alice # => true
Para fornecer outro exemplo, suponha que queremos rastrear o histórico de preços de produtos e pacotes. Isso pode ser obtido por meio de uma associação polimórfica incorporada de um para muitos:
class Product include Mongoid::Document field :name, type: String has_and_belongs_to_many :bundles embeds_many :prices, as: :item end class Bundle include Mongoid::Document field :name, type: String has_and_belongs_to_many :products embeds_many :prices, as: :item end class Price include Mongoid::Document embedded_in :item, polymorphic: true end pants = Product.create!(name: 'Pants', prices: [Price.new, Price.new]) costume = Bundle.create!(name: 'Costume', products: [pants], prices: [Price.new, Price.new])
Para definir uma associação polimórfica, especifique a opção polymorphic: true
na associação filho e adicione a opção as: :association_name
à associação pai.
Observe que o Mongoid atualmente suporta o polimorfismo apenas em uma direção - do filho para o pai. Por exemplo, o polimorfismo não pode ser usado para especificar que um pacote pode conter outros pacotes ou produtos:
class Bundle include Mongoid::Document # Does not work: has_many :items, polymorphic: true end
has_and_belongs_to_many
associações não apoiam polimorfismo.
Retornos de chamada em cascata
Se quiser que as chamadas de resposta do documento incorporado sejam acionadas ao chamar uma operação de persistência em seu pai, você precisará fornecer a opção de chamadas de resposta em cascata para a associação.
class Band include Mongoid::Document embeds_many :albums, cascade_callbacks: true embeds_one :label, cascade_callbacks: true end band.save # Fires all save callbacks on the band, albums, and label.
Comportamento dependente
Você pode fornecer opções dependentes às associações referenciadas para instruir o Mongoid sobre como lidar com situações em que um lado da associação é excluído ou tenta ser excluído. As opções são as seguintes:
:delete_all
: Exclua o(s) documento(s) filho(s) sem executar nenhum dos retornos de chamada do modelo.:destroy
: Destruir o(s) documento(s) filho(s) e executar(s) todos os retornos de chamada do modelo.:nullify
: Define o campo de chave estrangeira do documento filho como nulo. O filho pode se tornar órfão se, normalmente, for referenciado apenas por meio do pai.:restrict_with_exception
:raise
um erro se a secundária não estiver vazia.:restrict_with_error
: Cancelar operação e retornar falso se o filho não estiver vazio.
Se nenhuma opção :dependent
for fornecida, a exclusão do documento pai deixará o documento filho inalterado (em outras palavras, o documento filho continuará fazendo referência ao documento pai agora excluído por meio do campo de chave estrangeira). O filho pode ficar órfão se normalmente for referenciado apenas pelo pai.
class Band include Mongoid::Document has_many :albums, dependent: :delete_all belongs_to :label, dependent: :nullify end class Album include Mongoid::Document belongs_to :band end class Label include Mongoid::Document has_many :bands, dependent: :restrict_with_exception end label = Label.first label.bands.push(Band.first) label.delete # Raises an error since bands is not empty. Band.first.destroy # Will delete all associated albums.
Salvamento automático
Uma diferença fundamental entre o Mongoid e o ActiveRecord é que o Mongoid não salva automaticamente os documentos associados para associações referenciadas (ou seja, não incorporadas) quando o principal é salvo, por motivos de desempenho.
Se o salvamento automático não for usado, é possível criar referências pendentes a documentos inexistentes por meio de associações:
class Band include Mongoid::Document has_many :albums end class Album include Mongoid::Document belongs_to :band end band = Band.new album = Album.create!(band: band) # The band is not persisted at this point. album.reload album.band_id # => BSON::ObjectId('6257699753aefe153121a3d5') # Band does not exist. album.band # => nil
Para fazer com que as associações referenciadas sejam salvas automaticamente quando o pai for salvo, adicione a opção :autosave
à associação:
class Band include Mongoid::Document has_many :albums end class Album include Mongoid::Document belongs_to :band, autosave: true end band = Band.new album = Album.create!(band: band) # The band is persisted at this point. album.reload album.band_id # => BSON::ObjectId('62576b4b53aefe178b65b8e3') album.band # => #<Band _id: 62576b4b53aefe178b65b8e3, >
A funcionalidade de salvamento automático é adicionada automaticamente a uma associação ao usar accepts_nested_attributes_for
, para que o aplicativo não precise controlar quais associações foram modificadas ao processar um envio de formulário.
Associações incorporadas sempre salvam automaticamente, pois são armazenadas como parte do documento pai.
Algumas operações em associações sempre salvam os documentos pai e filho como parte da operação, independentemente de o salvamento automático estar habilitado. Uma lista não exaustiva dessas operações é a seguinte:
Atribuição à associação:
# Saves the band and the album. band.albums = [Album.new] push
,<<
:band.albums << Album.new band.albums.push(Album.new)
Predicados de existência
Todas as associações têm predicados de existência sobre elas na forma de name?
e has_name?
para verificar se a associação está em branco.
class Band include Mongoid::Document embeds_one :label embeds_many :albums end band.label? band.has_label? band.albums? band.has_albums?
Construção automática
Associações um para um (embeds_one
, has_one
) têm uma opção de auto-construção que pede ao Mongoid para instanciar um novo documento quando a associação é acessada e é nil
.
class Band include Mongoid::Document embeds_one :label, autobuild: true has_one :producer, autobuild: true end band = Band.new band.label # Returns a new empty label. band.producer # Returns a new empty producer.
Tocante
Qualquer associação belongs_to
pode ter uma opção :touch
opcional que fará com que o documento pai seja tocado sempre que o documento filho for atualizado:
class Band include Mongoid::Document field :name belongs_to :label, touch: true end band = Band.first band.name = "The Rolling Stones" band.save! # Calls touch on the parent label. band.touch # Calls touch on the parent label.
:touch
também pode receber um argumento do tipo string ou símbolo que especifica um campo a ser tocado na associação pai, além de updated_at:
class Label include Mongoid::Document include Mongoid::Timestamps field :bands_updated_at, type: Time has_many :bands end class Band include Mongoid::Document belongs_to :label, touch: :bands_updated_at end label = Label.create! band = Band.create!(label: label) band.touch # Updates updated_at and bands_updated_at on the label.
Quando um documento incorporado é tocado, seus pais são recursivamente tocados através da raiz de composição (porque todos os pais são necessariamente salvos quando o documento incorporado é salvo). O atributo :touch
, portanto, é desnecessário em associações embedded_in
.
Atualmente, o Mongoid não suporta a especificação de um campo adicional a ser tocado em uma associação embedded_in.
:touch
não deve ser definido como false
em uma associação embedded_in
, uma vez que a hierarquia de composição é sempre atualizada com um toque em um documento incorporado. Atualmente, esta medida não é aplicada, mas a aplicação destina-se a ser aplicada no futuro .
A opção counter_cache
Assim como no ActiveRecord, a opção :counter_cache
pode ser usada em uma associação para tornar mais eficiente a localização do número de objetos pertencentes. Também semelhante ao ActiveRecord, você deve levar em conta que haverá um atributo extra no modelo associado. Isso significa que, com o Mongoid, você precisa incluir Mongoid::Attributes::Dynamic
no modelo associado. Por exemplo:
class Order include Mongoid::Document belongs_to :customer, counter_cache: true end class Customer include Mongoid::Document include Mongoid::Attributes::Dynamic has_many :orders end
Proxies de associação
As associações empregam proxies transparentes para os objetos de destino. Isso pode causar comportamento surpreendente em algumas situações.
A visibilidade do método pode ser perdida quando os métodos nas metas de associação são acessados, dependendo da associação:
class Order include Mongoid::Document belongs_to :customer private def internal_status 'new' end end class Customer include Mongoid::Document has_many :orders private def internal_id 42 end end order = Order.new customer = Customer.create!(orders: [order]) # has_many does not permit calling private methods on the target customer.orders.first.internal_status # NoMethodError (private method `internal_status' called for #<Order:0x000055af2ec46c50>) # belongs_to permits calling private methods on the target order.customer.internal_id # => 42
Metadados de associação
Todas as associações no Mongoid contêm metadados que contêm informações sobre a associação em questão e são uma ferramenta valiosa para desenvolvedores terceirizados usarem para estender o Mongoid.
Você pode acessar os metadados da associação de algumas maneiras diferentes.
# Get the metadata for a named association from the class or document. Model.reflect_on_association(:association_name) model.reflect_on_association(:association_name) # Get the metadata with a specific association itself on a specific # document. model.associations[:association_name]
Atributos
Todas as associações contêm um _target
, que é o documento ou os documentos procurados, um _base
, que é o documento do qual a associação depende, e um _association
, que fornece informações sobre a associação.
class Person include Mongoid::Document embeds_many :addresses end person.addresses = [ address ] person.addresses._target # returns [ address ] person.addresses._base # returns person person.addresses._association # returns the association metadata
O objeto de associação
O objeto de associação em si contém mais informações do que se poderia imaginar e é útil para desenvolvedores de extensões para o Mongoid.
Método | Descrição |
---|---|
Association#as | Retorna o nome do pai para um filho polimórfico. |
Association#as? | Retorna se existe ou não uma opção. |
Association#autobuilding? | Retorna se a associação é de construção automática ou não. |
Association#autosaving? | Retorna se a associação está ou não salvando automaticamente. |
Association#cascading_callbacks? | Retorna se a associação tem retornos de chamada em cascata do pai. |
Association#class_name | Retorna o nome da classe do documento com proxy. |
Association#cyclic? | Retorna se a associação é uma associação cíclica. |
Association#dependent | Retorna a opção dependente da associação. |
Association#destructive? | Retorna verdadeiro se a associação tiver uma exclusão dependente ou destruir. |
Association#embedded? | Retorna se a associação está incorporada em outro documento. |
Association#forced_nil_inverse? | Retorna se a associação tem um inverso nulo definido. |
Association#foreign_key | Gera o nome do campo de chave externa. |
Association#foreign_key_check | Retorna o nome do método de verificação suja do campo de chave estrangeira. |
Association#foreign_key_setter | Retorna o nome do configurador de campos de chave estrangeira. |
Association#indexed? | Retorna se a chave estrangeira é indexada automaticamente. |
Association#inverses | Retorna os nomes de todas as associações inversas. |
Association#inverse | Retorna o nome de uma única associação inversa. |
Association#inverse_class_name | Retorna o nome da classe da associação no lado inverso. |
Association#inverse_foreign_key | Retorna o nome do campo da chave estrangeira no lado inverso. |
Association#inverse_klass | Retorna a classe da associação no lado inverso. |
Association#inverse_association | Gera os metadados da associação no lado inverso. |
Association#inverse_of | Retorna o nome explicitamente definido da associação inversa. |
Association#inverse_setter | Retorna o nome do método utilizado para definir a inversa. |
Association#inverse_type | Retorna o nome do campo do tipo polimórfico da inversa. |
Association#inverse_type_setter | Retorna o nome do conjunto de campos do tipo polimórfico do inverso. |
Association#key | Retorna o nome do campo no hash de atributos a ser usado para obter a associação. |
Association#klass | Gera a classe dos documentos com proxy na associação. |
Association#name | Retorna o nome da associação. |
Association#options | Gera a si mesmo, para fins de compatibilidade de API com o ActiveRecord. |
Association#order | Retorna as opções de classificação personalizada na associação. |
Association#polymorphic? | Retorna se a associação é polimórfica. |
Association#setter | Retorna o nome do campo para definir a associação. |
Association#store_as | Retorna o nome do atributo para armazenar uma associação incorporada. |
Association#touchable? | É gerado se a associação tem ou não uma opção de toque. |
Association#type | Retorna o nome do campo para obter o tipo polimórfico. |
Association#type_setter | Retorna o nome do campo para definir o tipo polimórfico. |
Association#validate? | Retorna se a associação tem uma validação associada. |