Menu Docs
Página inicial do Docs
/ / /
Mongoid
/

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

O Mongoid suporta as associações has_one, has_many, belongs_to e has_and_belongs_to_many familiares aos usuários do ActiveRecord.

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

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

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).

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 faz any?, 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

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

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.

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})

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.

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.

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

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

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.

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

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

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

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

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.

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)

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:

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})

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ção InvalidQuery 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.

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.

O Mongoid oferece três métodos para excluir filhos de associações embeds_many: clear, destroy_all e delete_all.

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
# => []

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

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

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.

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 ]

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

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.

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 ou

  • um 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.

É 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

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.

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.

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.

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)

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?

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.

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 .

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

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

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]

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 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.

Voltar

Herança