Trabalhando com o padrão de coleção única do MongoDB em Swift
Avalie esse Início rápido
É um axioma do MongoDB que você obtém o melhor desempenho e escalabilidade armazenando juntos os dados que são mais comumente acessados em conjunto.
A abordagem mais simples e óbvia para conseguir isso é incorporar todos os dados relacionados em um único documento. Isso funciona muito bem em muitos casos, mas há alguns cenários em que pode se tornar ineficiente:
- Relacionamentos de muitos para muitos (muito). Isso pode levar a dados duplicados. Essa duplicação geralmente é aceitável – apesar de tudo, o armazenamento é comparativamente barato. Fica mais doloroso quando os dados duplicados são modificados com frequência. Em seguida, você tem o custo de atualizar todos os documentos que incorporam esses dados.
- Leitura de pequenas partes de documentos grandes. Mesmo que sua query esteja interessado apenas em uma pequena fração de campos em um documento, todo o documento é colocado no cache — ocupando memória que poderia ser usada de forma mais eficaz.
- Documentos grandes e mutáveis. Sempre que seu aplicativo faz uma alteração em um documento, todo o documento deve ser gravado em disco em algum momento (pode ser combinado com outras alterações no mesmo documento). O WiredTiger grava dados no disco em blocos de 4 KB após a compressão - o que normalmente mapeia para um documento não compactado de 16a20 KB. Se você estiver fazendo muitas edições pequenas em um documento 20+ KB, pode estar desperdiçando E/S de disco.
Se incorporar todos os dados em um único documento não for o padrão certo para seu aplicativo, considere o design de collection única. O padrão de collection única pode oferecer desempenho de leitura comparável ao de documentos incorporados, além de otimizar as atualizações.
Existem variantes no padrão de collection única, mas para esta publicação, concentrei-me nos aspectos principais:
- Os dados relacionados consultados juntos são armazenados na mesma collection.
- Os documentos podem ter estruturas diferentes.
- Os índices são adicionados para que todos os dados de suas queries frequentes possam ser obtidos com uma única pesquisa de índice.
Nesse momento, seu cérebro de desenvolvedor pode estar levantando questões sobre como o código do seu aplicativo pode lidar com isso. É comum ler os dados de uma collection específica e, em seguida, fazer com que o driver do MongoDB converta esse documento em um objeto de uma classe específica. Como isso funciona se o driver estiver buscando documentos com diferentes formas da mesma collection? Essa é a principal coisa que gostaria de demonstrar nesta postagem.
Usarei o Swift, mas os mesmos princípios se aplicam a outros idiomas. Para ver como fazer isso com Java/Spring Data, dê uma olhada em Designs de collection no MongoDB com Spring Data.
Comece recentemente a usar o MongoDB Swift Driver pela primeira vez. Decidi criar um aplicativo de desktop Mac supersimples que permita navegar por suas coleções (o que o MongoDB Compass faz um trabalhomuito melhor) e exibir eventos de Change Stream em tempo real (o que o Compass não faz atualmente).
Você pode baixar o código do repositório Swift-Change-Streams. Basta criar e executar a partir do Xcode .
Forneça sua connection string e navegue pelas suas collections. Selecione a opção " Ativar fluxos de alteração " para exibir eventos de alteração em tempo real.
O aplicativo exibirá dados da maioria das collection como documentos JSON genéricos, sem conhecimento do esquema. Há um caso especial para uma collection chamada "Collection" em um banco de dados chamado "Single" — vamos ver isso a seguir.
A collection Simple.Collection precisa conter estes documentos (ou semelhantes):
1 { _id: 'basket1', docType: 'basket', customer: 'cust101' } 2 { _id: 'basket1-item1', docType: 'item', name: 'Fish', quantity: 5 } 3 { _id: 'basket1-item2', docType: 'item', name: 'Chips', quantity: 3 }
Esses dados representam uma bandeja de compras com uma
_id
de "cesta1". Existem dois itens associados a basket1 — basket1-item1
e basket1-item2
. Uma única query buscará todos os três documentos para a bandeja (encontre todos os documentos onde _id
começa com "cesta1"). Sempre há um índice no atributo_id
e, portanto, esse índice será usado.Observe que todos os dados de uma cesta nesse conjunto de dados são extremamente pequenos - bem abaixo do limite 16-20K - e, portanto, em um exemplo real, eu aconselharia incorporar tudo em um único documento. O padrão de collection única faria mais sentido se houvesse um grande número de itens de linha e cada um deles fosse grande (por exemplo, se fossem incorporadas várias imagens em miniatura).
Cada documento também tem um atributo
docType
para identificar se o documento se refere à própria cesta ou a um dos itens associados. Se o seu aplicativo incluísse uma consulta comum para buscar apenas a cesta ou apenas os itens associados à cesta, você poderia adicionar um índice composto: { _id: 1, docType: 1}
.Outros usos do campo
docType
incluem:- Um prompt para ajudar os humanos a entender o que estão vendo na collection.
- Filtrar os dados retornados de uma query apenas para determinados tipos de documentos da collection.
- Filtrar quais tipos de documentos são incluídos ao usar o MongoDB Compass para examinar o schema de uma collection.
- Permitir que um aplicativo identifique o tipo de documento recebido. O código do aplicativo pode fazer com que o driver MongoDB desmarque o documento em um objeto da classe correta. Isso é o que veremos a seguir.
Usaremos o mesmo aplicativo de desktop para ver como seu código pode discriminar entre diferentes tipos de documentos da mesma collection.
O aplicativo tem conhecimento codificado da aparência de uma cesta e de documentos de itens. Isso permite renderizar os dados do documento em formatos específicos, em vez de um JSON document:
O código para determinar o documento
docType
e converter o documento em um objeto da classe apropriada pode ser encontrado em CollectionView.swift.CollectionView busca todos os documentos correspondentes do MongoDB e os armazena em uma array de
BSONDocument
s:1 private var docs = [BSONDocument]()
O aplicativo pode então fazer um loop sobre cada documento em
docs
, verificar o atributodocType
e decidir o que fazer com base nesse valor:1 List(docs, id: \.hashValue) { doc in 2 if path.dbName == "Single" && path.collectionName == "Collection" { 3 if let docType = doc["docType"] { 4 switch docType { 5 case "basket": 6 if let basket = basket(doc: doc) { 7 BasketView(basket: basket) 8 } 9 case "item": 10 if let item = item(doc: doc) { 11 ItemView(item: item) 12 } 13 default: 14 Text("Unknown doc type") 15 } 16 } 17 } else { 18 JSONView(doc: doc) 19 } 20 }
Se
docType == "basket"
, o código converte o documento genérico em um objetoBasket
e o passa para BasketView
para renderização.Esta é a classe
Basket
, incluindo o inicializador para criar umBasket
a partir de um BSONDocument
:1 struct Basket: Codable { 2 let _id: String 3 let docType: String 4 let customer: String 5 6 init(doc: BSONDocument) { 7 do { 8 self = try BSONDecoder().decode(Basket.self, from: doc) 9 } catch { 10 _id = "n/a" 11 docType = "basket" 12 customer = "n/a" 13 print("Failed to convert BSON to a Basket: \(error.localizedDescription)") 14 } 15 } 16 }
Da mesma forma para
Item
s:1 struct Item: Codable { 2 let _id: String 3 let docType: String 4 let name: String 5 let quantity: Int 6 7 init(doc: BSONDocument) { 8 do { 9 self = try BSONDecoder().decode(Item.self, from: doc) 10 } catch { 11 _id = "n/a" 12 docType = "item" 13 name = "n/a" 14 quantity = 0 15 print("Failed to convert BSON to a Item: \(error.localizedDescription)") 16 } 17 } 18 }
As subexibições podem então usar os atributos do objeto com o tipo correto para renderizar os dados adequadamente:
1 struct BasketView: View { 2 let basket: Basket 3 4 var body: some View { 5 VStack { 6 Text("Basket") 7 .font(.title) 8 Text("Order number: \(basket._id)") 9 Text("Customer: \(basket.customer)") 10 } 11 .padding() 12 .background(.secondary) 13 .clipShape(RoundedRectangle(cornerRadius: 15.0)) 14 } 15 }
1 struct ItemView: View { 2 let item: Item 3 4 var body: some View { 5 VStack { 6 Text("Item") 7 .font(.title) 8 Text("Item name: \(item.name)") 9 Text("Quantity: \(item.quantity)") 10 } 11 .padding() 12 .background(.secondary) 13 .clipShape(RoundedRectangle(cornerRadius: 15.0)) 14 } 15 }
O padrão de coleção única é uma maneira de fornecer desempenho de leitura e gravação quando a incorporação ou outros padrões de design não são adequados.
Esse padrão quebra o mapeamento 1-1 entre classes de aplicativos e collections do MongoDB que muitos desenvolvedores podem presumir. Esta publicação mostra como contornar isso:
- Extraia um único campo docType do documento BSON retornado pelo driver do MongoDB.
- Verifique o valor de docType e faça com que o driver MongoDB mapeie o documento BSON em um objeto da classe apropriada.