Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Saiba por que o MongoDB foi selecionado como um líder no 2024 Gartner_Magic Quadrupnt()
Desenvolvedor do MongoDB
Centro de desenvolvedores do MongoDB
chevron-right
Produtos
chevron-right
MongoDB
chevron-right

Codificação com Mark: abstraindo junções e subconjuntos em Python

Mark Smith11 min read • Published Mar 19, 2024 • Updated Mar 19, 2024
MongoDBFramework de agregaçãoPython
SNIPPET
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Este tutorial falará sobre os padrões de design do MongoDB — especificamente, o padrão de subconjunto — e mostrará como você pode criar uma abstração em seu modelo de dados Python que oculta como os dados são realmente modelados em seu banco de dados.
Este é o terceiro tutorial de uma série! Sinta-se à vontade para conferir o primeiro ou osegundo tutorial, se quiser, mas não é necessário se você quiser apenas continuar lendo.

Codificando com Mark?

Este tutorial é vagamente baseado em alguns episódios de uma transmissão ao vivo que apresento, chamada "Coding with Mark". Estou transmitindo às quartas-feiras às 2 GMT (ou seja, 9 ET ou 6 PT, se você acordar cedo!). Se esse tempo não funcionar para você, você sempre pode acompanhar assistindo às gravações!
Atualmente, estou criando uma biblioteca experimental de camada de acesso a dados que deve fornecer um kit de ferramentas para abstrair modelos de documentos complexos da camada de lógica comercial do aplicativo que os está usando.
Você pode conferir o código no repositório do GitHub do projeto!

Montando o cenário

O objetivo do docbridge, meu mapeador objeto-documento, é abstrair o modelo de dados usado no MongoDB do modelo de dados usado por um programa Python. Com uma base de código de qualquer tamanho, você precisa de algo assim porque, caso contrário, toda vez que você alterar seu modelo de dados (em seu banco de dados de dados), você precisará alterar o modelo de objetos (em seu código). Por ter uma camada de abstração, você localiza todo esse mapeamento em uma única área de sua base de código, e essa é a única parte que precisa mudar quando você muda seu modelo de dados. Essa capacidade de alterar seu modelo de dados realmente permite que você aproveite a flexibilidade do modelo de documento do MongoDB.
No primeiro tutorial, mostrei uma abstração bem simples, o FallbackField, que tentaria vários nomes de campos diferentes em um documento até encontrar um que existisse, e então retornaria esse valor. Esta foi uma implementação muito simples do padrão Schema Versioning.
Neste tutorial, vou abstrair algo mais complexo: o Subset Pattern.

O padrão de subconjunto

O MongoDB permite armazenar arrays em seus documentos, nativamente. Os valores nessas arrays podem ser tipos primitivos, como números, strings, datas ou até mesmo subdocumentos. Mas, às vezes, essas arrays podem ficar muito grandes, e o padrão de subconjunto descreve uma técnica em que o subconjunto mais importante da array (geralmente apenas os primeiros itens) é armazenado diretamente na array incorporada, e quaisquer itens excedentes são armazenados em outros documentos e procurava apenas quando necessário.
Isso resolve dois problemas de design: primeiro, recomendamos que você não armazene mais do que itens 200 em uma matriz, pois quanto mais itens você tiver, mais lento será o banco de dados em percorrer os campos em cada documento. Em segundo lugar, o padrão de subconjunto também responde a uma pergunta que vi muitas vezes quando ensinamos modelagem de dados: "Como faço para impedir que minha matriz cresça tanto que o documento se torne maior do que o limite de 16MB?" Já que estamos falando do assunto, evite que seus documentos fiquem tão grandes - isso geralmente implica que você poderia melhorar seu modelo de dados, por exemplo, separando os dados em documentos separados ou, se estiver armazenando muitos dados binários, poderia mantê-los fora do banco de dados, em um armazenamento de objetos.

Implementação do tipo SequenceField

Antes de analisar como abstrair uma pesquisa para os itens extras da array que não estão incorporados ao documento de origem, primeiro implementarei um tipo wrapper para uma array BSON. Isso pode ser usado para declarar campos de array em uma classeDocument, em vez do tipoField que implementei em artigos anteriores.
Vamos definir um SequenceField para mapear a array de um documento no modelo de objeto da minha camada de acesso. A funcionalidade principal de um SequenceField é que você pode especificar um tipo para os itens da array e, quando você iterar pela sequência, ele retornará objetos desse tipo, em vez de apenas produzir o tipo armazenado no documento.
Um exemplo concreto seria a classe UserProfile de uma API de mídia social, que armazenaria uma lista de objetos de seguidor. Criei alguns documentos de amostra com um roteiro Python usando Faker. Um documento de exemplo tem a seguinte aparência:
1{
2  "_id": { "$oid": "657072b56731c9e580e9dd70" },
3  "user_id": "4",
4  "user_name": "@tanya15",
5  "full_name": "Deborah White",
6  "birth_date": { "$date": { "$numberLong": "931219200000" } },
7  "email": "deanjacob@yahoo.com",
8  "bio": "Music conference able doctor degree debate. Participant usually above relate.",
9  "follower_count": { "$numberInt": "59" },
10  "followers": [
11    {
12      "_id": { "$oid": "657072b66731c9e580e9dda6" },
13      "user_id": "58",
14      "user_name": "@rduncan",
15      "bio": "Rich beautiful color life. Relationship instead win join enough board successful."
16    },
17    {
18      "_id": { "$oid": "657072b66731c9e580e9dd99" },
19      "user_id": "45",
20      "user_name": "@paynericky",
21      "bio": "Picture day couple democratic morning. Environment manage opportunity option star food she. Occur imagine population single avoid."
22 },
23 # ... other followers
24 ]
25}
Posso modelar esses dados usando duas classes: uma para os dados de perfil de nível superior e outra para os dados de resumo dos seguidores desse perfil (incorporados na matriz).
1class Follower(Document):
2    _id = Field(transform=str)
3    user_name = Field()
4
5class Profile(Document):
6    _id = Field(transform=str)
7    followers = SequenceField(type=Follower)
Se eu quiser percorrer todos os seguintes de uma instância de perfil, cada item deve ser uma instânciaFollower:
1profile = Profile(SOME_BSON_DATA)
2for follower in profile.followers:
3        assert isinstance(follower, Follower)
Este comportamento pode ser implementado de forma semelhante à classeField, implementando-o como um descritor, com um método__get__ que, nesse caso, produz um Follower construído para cada item na array BSON subjacente. O código é um pouco assim:
1class SequenceField:
2    """
3    Allows an underlying array to have its elements wrapped in
4    Document instances.
5    """
6
7    def __init__(
8        self,
9        type,
10        field_name=None,
11    ):
12        self._type = type
13        self.field_name = field_name
14
15    def __set_name__(self, owner, name):
16        """
17        Called when the enclosing Document subclass (owner) is defined.
18        """
19        self.name = name  # Store the attribute name.
20
21        # If a field-name mapping hasn't been provided,
22        # the BSON field will have the same name as the attribute name.
23        if self.field_name is None:
24            self.field_name = name
25
26    def __get__(self, ob, cls):
27        """
28        Called when the SequenceField attribute is accessed on the enclosed
29        Document subclass.
30        """
31        try:
32            # Lookup the field in the BSON, and return an array where each item
33            # is wrapped by the class defined as type in __init__:
34            return [
35                self._type(item, ob._db)
36                for item in ob._doc[self.field_name]
37                ]
38        except KeyError as ke:
39            raise ValueError(
40                f"Attribute {self.name!r} is mapped to missing document property {self.field_name!r}."
41            ) from ke
Isso é muito código, mas grande parte dele é duplicado a partir de Field - Vou corrigir isso com alguma herança em algum momento. A parte mais importante está próxima ao final:
1return [
2
3    self._type(item, ob._db)
4
5    for item in ob._doc[self.field_name]
6]
No exemplo concreto acima, isso resultaria em algo como este código fictício:
1return [
2   Follower(item, db=None) for item in profile._doc["followers"]
3]

Adicionando seguidor extra

O conjunto de dados que criei para trabalhar com isso armazena apenas os primeiros 20 seguidores em um documento de perfil. O restante é armazenado em uma collection "seguidores" e é agrupado para armazenar até 20 seguintes por documento, em um campo chamado "seguidores". O campo "user_id" diz a quem pertencem os membros. Um único documento na collection "seguidores" tem a seguinte aparência:
Um documento contendo um campo " followers " que contém mais alguns seguidores para o usuário com " user_id " de "4"
O Padrão de Bucket é uma técnica para colocar muitos subdocumentos pequenos juntos em um documento de bucket, o que pode tornar mais eficiente a recuperação de documentos que geralmente são recuperados juntos, e pode manter os tamanhos dos índices baixos. A desvantagem é que isso torna a atualização de subdocumentos individuais um pouco mais lenta e mais complexa.

Como consultar documentos em buckets

Tenho uma coleção onde cada documento contém uma array de membros — um "balde" de membros. Mas o que eu preciso é uma query que retorne documentos individuais de seguidor. Vamos detalhar como essa query funcionará:
  1. Pretendo procurar todos os documentos para um user_id específico.
  2. Para cada item em seguidores - cada item é um seguidor - quero gerar um único documento para esse seguidor.
  3. Queremos reestruturar cada documento para que ele  contenhaapenas as informações do seguidor, não as informações do bucket.
Isso é o que eu amo nos pipelines de agregação — depois de criar essas etapas, muitas vezes posso converter cada etapa em um estágio de pipeline de agregação.
Etapa 1: Procure todos os documentos de um usuário específico:
1 {"$match": {"user_id": "4"}}
Observe que este estágio codificou o valor "4" para o campo "user_id". Explicarei mais tarde como valores dinâmicos podem ser inseridos nessas queries. Isso gera um único documento, um bucket, contendo muitos membros, em um campo chamado "seguidores":
1{
2  "user_name": "@tanya15",
3  "full_name": "Deborah White",
4  "birth_date": {
5    "$date": "1999-07-06T00:00:00.000Z"
6  },
7  "email": "deanjacob@yahoo.com",
8  "bio": "Music conference able doctor degree debate. Participant usually above relate.",
9  "user_id": "4",
10  "follower_count": 59,
11  "followers": [
12    {
13      "_id": {
14        "$oid": "657072b66731c9e580e9dda6"
15      },
16      "user_id": "58",
17      "user_name": "@rduncan",
18      "bio": "Rich beautiful color life. Relationship instead win join enough board successful."
19    },
20    {
21      "bio": "Picture day couple democratic morning. Environment manage opportunity option star food she. Occur imagine population single avoid.",
22      "_id": {
23        "$oid": "657072b66731c9e580e9dd99"
24      },
25      "user_id": "45",
26      "user_name": "@paynericky"
27    },
28    {
29      "_id": {
30        "$oid": "657072b76731c9e580e9ddba"
31      },
32      "user_id": "78",
33      "user_name": "@tiffanyhicks",
34      "bio": "Sign writer win. Look television official information laugh. Lay plan effect break expert message during firm."
35    },
36   . . .
37  ],
38  "_id": {
39    "$oid": "657072b56731c9e580e9dd70"
40  }
41}
Etapa 2: produza um documento para cada seguidor — o estágio $unwind pode fazer exatamente isso:
1{"$unwind": "$followers"}
Isso instrui o MongoDB a retornar um documento para cada item da array "followers". Todo o conteúdo do documento será incluído, mas a array de membros será substituída pelo subdocumento de seguidor único a cada vez. Isso gera vários documentos, cada um contendo um único seguidor no campo"seguidores":
1# First document:
2{
3  "bio": "Music conference able doctor degree debate. Participant usually above relate.",
4  "follower_count": 59,
5  "followers": {
6    "_id": {
7      "$oid": "657072b66731c9e580e9dda6"
8    },
9    "user_id": "58",
10    "user_name": "@rduncan",
11    "bio": "Rich beautiful color life. Relationship instead win join enough board successful."
12  },
13  "user_id": "4",
14  "user_name": "@tanya15",
15  "full_name": "Deborah White",
16  "birth_date": {
17    "$date": "1999-07-06T00:00:00.000Z"
18  },
19  "email": "deanjacob@yahoo.com",
20  "_id": {
21    "$oid": "657072b56731c9e580e9dd70"
22  }
23}
24
25# Second document
26{
27  "_id": {
28    "$oid": "657072b56731c9e580e9dd70"
29  },
30  "full_name": "Deborah White",
31  "email": "deanjacob@yahoo.com",
32  "bio": "Music conference able doctor degree debate. Participant usually above relate.",
33  "follower_count": 59,
34  "user_id": "4",
35  "user_name": "@tanya15",
36  "birth_date": {
37    "$date": "1999-07-06T00:00:00.000Z"
38  },
39  "followers": {
40    "_id": {
41      "$oid": "657072b66731c9e580e9dd99"
42    },
43    "user_id": "45",
44    "user_name": "@paynericky",
45    "bio": "Picture day couple democratic morning. Environment manage opportunity option star food she. Occur imagine population single avoid."
46  }
47
48# . . . More documents follow
Etapa 3: reestruture o documento, extraindo o valor "seguidor" até o nível superior do documento. Há um estágio especial para fazer isso — $replaceRoot:
1{"$replaceRoot": {"newRoot": "$followers"}},
Adicionar o estágio acima resulta em cada documento contendo um único seguidor, no nível superior:
1# Document 1:
2{
3  "_id": {
4    "$oid": "657072b66731c9e580e9dda6"
5  },
6  "user_id": "58",
7  "user_name": "@rduncan",
8  "bio": "Rich beautiful color life. Relationship instead win join enough board successful."
9}
10
11# Document 2
12{
13  "_id": {
14    "$oid": "657072b66731c9e580e9dd99"
15  },
16  "user_id": "45",
17  "user_name": "@paynericky",
18  "bio": "Picture day couple democratic morning. Environment manage opportunity option star food she. Occur imagine population single avoid."
19}
20} # . . . More documents follow
Resumindo, a consulta se parece com isto:
1[
2    {"$match": {"user_id": "4"}},
3    {"$unwind": "$followers"},
4    {"$replaceRoot": {"newRoot": "$followers"}},
5]
Eu expliquei a consulta que eu quero que seja executada toda vez que eu fizer uma iteração no campo de seguidores na minha biblioteca de abstração de dados. Agora, mostrarei como ocultar essa query (ou qualquer query necessária) na implementação de SequenceField.

Resumo da pesquisa

Agora, gostaria de alterar o comportamento do SequenceField para que ele faça o seguinte:
  • Percorra os subdocumentos incorporados e produza cada um deles, agrupado por tipo (o callable que envolve cada subdocumento).
  • Se o usuário chegar ao final da matriz incorporada, faça uma consulta para procurar o restante dos seguidores e produzi-los um por um, também agrupados por tipo.
Primeiro, alterarei o  método__init__para que o usuário possa fornecer dois parâmetros adicionais:
  • A coleção que contém os documentos extras, superset_collection
  • A query a ser executada nessa collection para retornar documentos individuais, superset_query
O resultado é:
1class Field:
2    def __init__(
3        self,
4        type,
5        field_name=None,
6        superset_collection=None,
7        superset_query: Callable = None,
8    ):
9        self._type = type
10        self.field_name = field_name
11        self.superset_collection = superset_collection
12        self.superset_query = superset_query
A consulta terá de ser fornecida como um chamável, ou seja, uma função, expressão lambda ou método. A razão para isso é que a geração da consulta normalmente precisará de acesso a parte do estado do documento (nesse caso, o user_id, para construir a consulta e procurar os documentos seguidores corretos). O chamável é armazenado na instância do campo e, em seguida, quando a pesquisa é necessária, ele chama o chamável, passando-lhe o documento que contém o campo, para que o chamável possa procurar o usuário "_id" no dicionário_docagrupado.
Agora que o usuário pode fornecer informações suficientes para procurar os membros extras (o superconjunto), alterei o __get__ método para realizar a pesquisa quando ela ficar sem membros incorporados. Para tornar isso mais simples de escrever, aproveitei a inatividade. Duas vezes! Veja como:
Preguiça parte 1: Quando você executa uma consulta ligando para find aggregateou, a consulta não é executada imediatamente. Em vez disso, o método retorna imediatamente um cursor. Os cursores são preguiçosos – o que significa que eles não fazem nada até que você comece a usá-los, iterando sobre seu conteúdo. Assim que você começar a iterar, ou fazer um loop, sobre o cursor, ele  consultará o banco de dados de dados e começará a produzir resultados.
Preguiça parte 2: A maioria das funções no módulo principal do Python itertools também é preguiçosa, incluindo a chain função . A cadeia é chamada com um ou mais iteráveis como argumentos e, em seguida, começa a percorrer os argumentos posteriores quando os iteráveis anteriores se esgotam (o que significa que o código percorreu todo o conteúdo do iterável).
Eles podem ser combinados para criar um único iterável que nunca solicitará nenhum seguidor extra do banco de banco de dados, a menos que o código solicite especificamente mais itens após percorrer os itens incorporados:
1embedded_followers = self._doc["followers"] # a list
2cursor = followers.find({"user_id": "4"})   # a lazy database cursor
3
4# Looping through all_followers will only make a database call if you have
5# looped through all of the contents of embedded_followers:
6all_followers = itertools.chain(embedded_followers, cursor)
O código real é um pouco mais flexível, pois suporta consultas de busca e agregação. Ele reconhece o tipo porque as consultas find são fornecidas como dicts e as consultas agregadas são listas.
1def __get__(self, ob, cls):
2    if self.superset_query is None:
3        # Use an empty sequence if there are no extra items.
4        # It's still iterable, like a cursor, but immediately exits.
5        superset = []
6    else:
7        # Call the superset_query callable to obtain the generated query:
8        query = self.superset_query(ob)
9
10        # If the query is a mapping, it's a find query, otherwise it's an
11        # aggregation pipeline.
12        if isinstance(query, Mapping):
13            superset = ob._db.get_collection(self.superset_collection).find(query)
14        elif isinstance(query, Iterable):
15            superset = ob._db.get_collection(self.superset_collection).aggregate(
16                query
17            )
18        else:
19            raise Exception("Returned was not a mapping or iterable.")
20
21    try:
22        # Return an iterable that first yields all the embedded items, and
23
24        return chain(
25            [self._type(item, ob._db) for item in ob._doc[self.field_name]],
26            (self._type(item, ob._db) for item in superset),
27        )
28    except KeyError as ke:
29        raise ValueError(
30            f"Attribute {self.name!r} is mapped to missing document property {self.field_name!r}."
31        ) from ke
Adicionei alguns comentários ao código acima, então espero que você possa ver a relação entre o código simplificado acima dele e o código real aqui.

Usando o SequenceField para declarar relacionamentos

A implementação Profile e Follower agora é uma questão de fornecer a query (envolvida em uma expressão lambda) e a collection que deve ser queryda.
1# This is the same as it was originally
2class Follower(Document):
3    _id = Field(transform=str)
4    user_name = Field()
5
6
7def extra_followers_query(profile):
8    return [
9        {
10            "$match": {"user_id": profile.user_id},
11        },
12        {"$unwind": "$followers"},
13        {"$replaceRoot": {"newRoot": "$followers"}},
14    ]
15   
16class Profile(Document):
17    _id = Field(transform=str)
18    followers = SequenceField(
19        type=Follower,
20        superset_collection="followers",
21        superset_query=lambda ob: extra_followers_query,
22    )
Um aplicativo que usasse a  definição Profileacima poderia procurar oProfile com "user_id" de "4" e imprimir os nomes de usuário de todos os seus membros com um código como este:
1for follower in profile.followers:
2    print(follower.user_name)
Viu como a consulta extra agora faz parte da definição de mapeamento do tipo e não do código que lida com os dados? Esse é o tipo de abstração que eu queria fornecer quando comecei a construir essa biblioteca experimental. Tenho mais planos, então fique junto! Mas antes de implementar mais abstrações de dados, primeiro preciso implementar atualizações - isso é algo que descreverei em meu próximo tutorial.

Conclusão

Este é agora o terceiro tutorial da minha série de abstração de dados do Python, e devo reconhecer que este era o código que idealizei quando me ocorreu pela primeira vez a ideia da biblioteca docbridge. Foi muito gratificante chegar a esse ponto e, como venho desenvolvendo tudo com práticas de desenvolvimento orientadas a testes, já existe uma boa cobertura de código.
Se estiver procurando mais informações sobre aggregation pipelines, dê uma olhada em Aggregations práticas do MongoDB — ou agora, você pode comprar uma versão expandida do livro em papel.
Se você estiver interessado nos tópicos de abstração e na arquitetura de código Python em geral, pode comprar o livroPadrões de arquitetura com Python ou lê-lo online em CosmicPython.com
Eu transmito ao vivo na maioria das semanas, geralmente às 2 h UTC às quartas-feiras. Se isso lhe parece interessante, confira o canal doMongoDB no Youtube . Aguardo você lá!
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.
Iniciar a conversa

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Tutorial

Como se conectar ao MongoDB com um proxy SOCKS5 com Java


Aug 29, 2024 | 2 min read
Artigo

Como o Prisma analisa um esquema de um banco de dados MongoDB


May 19, 2022 | 8 min read
Tutorial

Como manter várias versões de um registro no MongoDB (atualizações 2024)


Aug 12, 2024 | 6 min read
Tutorial

Garantir alta disponibilidade para MongoDB no Kubernetes


Jul 12, 2024 | 11 min read
Sumário