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 .

Learn why MongoDB was selected as a leader in the 2024 Gartner® Magic Quadrant™
Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Produtoschevron-right
MongoDBchevron-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 MongoDB no do modelo de dados usado por um Python programa . 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), você precisará alterar o modelo de objeto (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 MongoDB do document model.
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 que você armazene matrizes em seus documentos, de forma nativa. Os valores nesses arrays podem ser tipos primitivos, como números, strings, datas ou até mesmo subdocumentos. Mas, às vezes, esses arrays podem ficar muito grandes, e o padrão de subconjunto descreve uma técnica em que o subconjunto mais importante do array (geralmente apenas os primeiros itens) é armazenado diretamente no array incorporado, e todos os itens excedentes são armazenados em outros documentos e consultados somente 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 em que cada documento contém uma matriz de seguidores - um balde "" de seguidores. Mas o que eu quero é uma consulta que retorne documentos individuais de seguidores. Vamos detalhar como essa consulta 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 arrayde membros será substituída pelo subdocumentode 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, puxando o valor "follower" até o nível superior do documento. Há uma etapa 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 pesquisar os seguidores extras (o superconjunto), alterei o  método__get__para realizar a pesquisa quando ficar sem seguidores incorporados. Para tornar isso mais simples de escrever, aproveitei a preguiça. Duas vezes! Veja como:
Preguiça Parte 1: Quando você executa uma consulta chamando find ou aggregate, 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ça a iterar ou fazer um loop sobre o cursor, ele  consulta o banco de dados e começa 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 funçãochain.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 dados, a menos que o código solicite especificamente mais itens depois de 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
Artigo

Como proteger o acesso aos dados do MongoDB com o Views


Apr 07, 2023 | 4 min read
Tutorial

Como implantar um aplicativo Spark com MongoDB no Fly.io


Dec 02, 2024 | 5 min read
Início rápido

MongoDB e Node.js 3.3.2 Tutorial - Operações CRUD


Oct 01, 2024 | 17 min read
Artigo

Usando MongoDB com Rust Web Development Framework


Aug 29, 2024 | 1 min read
Sumário