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

Criar uma camada de acesso a dados Python

Mark Smith12 min read • Published Dec 13, 2023 • Updated Jan 04, 2024
MongoDBPython
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Este tutorial mostrará como usar algumas técnicas razoavelmente avançadas de Python para agrupar documentos BSON de uma forma que os faça parecer muito mais com objetos Python e permita diferentes maneiras de acessar os dados neles contidos. É o primeiro de uma série que demonstra como criar uma camada de acesso a dados em Python parao MongoDB.

Codificando com Mark?

Este tutorial é vagamente baseado no primeiro episódio de uma nova transmissão ao vivo que eu apresento, chamada " Coding with Mark. " Estou transmitindo às quartas-feiras às da 2 tarde GMT (isso é 9 a.m. ET ou 6 a.m. PT, se você acorda cedo!). Se esse horário não funcionar para você, você sempre pode acompanhar assistindo às gravações!
Nos primeiros episódios, você pode acompanhar enquanto tento criar um tipo diferente de camada de acesso a dados Pythonic, uma biblioteca para abstrair as alterações subjacentes da modelagem de banco de dados de um aplicativo hipotético. Um dos exemplos que usarei mais adiante nesta série é uma plataforma de microblog, nos moldes do Twitter / X ou Bluesky. Para lidar com grandes volumes de dados, várias técnicas de modelagem são necessárias, e minha biblioteca tentará encontrar maneiras de tornar essas opções de modelagem de dados invisíveis para o aplicativo, facilitando o desenvolvimento e, ao mesmo tempo, permitindo alterar o modelo de dados subjacente.
Estou usando algumas técnicas avançadas de programação e metaprogramação para ocultar algumas funcionalidades muito inteligentes. Essa será uma boa série, independentemente de você estar procurando melhorar suas habilidades em Python ou MongoDB.
Se isso não parecer interessante o suficiente, estou alinhando alguns convidados incríveis da comunidade Python e, no futuro, podemos nos ramificar do Python e entrar em outros mundos estranhos e surpreendentes.

Por que uma camada de acesso a dados?

Em qualquer aplicativo bem arquitetado de tamanho razoável, você geralmente descobrirá que a base de código é dividida em pelo menos três áreas de preocupação:
  1. Uma camada de apresentação se preocupa com a formatação de dados para consumo por um cliente. Isso pode gerar páginas da Web a serem visualizadas por uma pessoa em um navegador, mas, cada vez mais, pode ser um ponto de extremidade de API, seja acionando um aplicativo executado no computador de um usuário (ou em seu navegador) ou fornecendo dados a outros serviços em uma arquitetura mais ampla baseada em serviços. Essa camada também é responsável por receber dados de um cliente e analisá-los em dados que podem ser usados pela camada de lógica de negócios.
  2. Uma camada lógica de negócios fica atrás da camada de apresentação e fornece o cérebro " " de um aplicativo, tomando decisões sobre quais ações tomar com base nas solicitações do usuário ou na entrada de dados no aplicativo.
  3. A camada de acesso a dados, onde vamos focar, fornece uma camada de abstração sobre o banco de dados. Sua responsabilidade é solicitar dados do banco de dados e fornecê-los em um formato utilizável para a camada de lógica de negócios, mas também receber solicitações da camada de lógica de negócios e armazenar adequadamente os dados no banco de dados.
Um diagrama que ilustra uma arquitetura de aplicativo clássica, com camadas de apresentação, lógica de negócios e acesso a dados.
Dado o foco deste artigo, vamos nos concentrar um pouco mais nas responsabilidades da camada de acesso a dados. Uma boa camada de acesso a dados fornecerá uma camada de abstração longe do armazenamento físico dos dados.
Com relational database, uma camada de acesso a dados pode ser uma implementação relativamente simples do padrão Active Record, correspondendo perfeitamente à forma como os dados são divididos em uma ou mais tabelas.
A flexibilidade do MongoDB, com a capacidade de armazenar documentos com esquemas diferentes em uma única coleção, e de escolher se deseja incorporar dados relacionados em um documento ou armazenar em outro lugar e participar de leitura, fornece mais poder do que os bancos de dados tabulares. Assim, uma camada de acesso a dados pode precisar fornecer mais recursos de abstração do que simplesmente portar um ORM para trabalhar com documentos. Um ORM é uma biblioteca de mapeamento objeto-relacional e lida com o mapeamento entre dados relacionais em um banco de dados tabular e objetos em seu aplicativo.

Por que não um ODM?

Boa pergunta! Muitos ODMs excelentes foram desenvolvidos para o MongoDB. ODM é a abreviação de "Mapeador de Documentos de Objeto" e descreve um tipo de biblioteca que tenta mapear entre os documentos do MongoDB e seus objetos de aplicativo. Apenas no ecossistema Python, existem MongoEngine, ODMantic, PyMODMe, mais recentemente, Goanie e Bunnet. Os dois últimos são mais ou menos iguais, mas oBeanie é construído em asyncio e Bunnet é síncrono. Estamos especialmente bem com o MongoDB e, como ele foi construído no Pydatatic, funciona especialmente bem com o FastAPI.
Por outro lado, a maioria dos ODMs está essencialmente resolvendo o mesmo problema – abstraindo a poderosa linguagem de query do MongoDB para facilitar a leitura e a escrita e modelar esquemas de documentos como objetos para que os dados possam ser serializados e desserializados diretamente entre o aplicativo e o MongoDB.
No entanto, depois que seu modelo de dados se tornar relativamente sofisticado, se você estiver implementando um ou mais padrões para melhorar o desempenho e a escalabilidade do aplicativo, a maneira como os dados serão armazenados não será necessariamente a maneira como você pensa logicamente sobre eles no aplicativo.
Além disso, se estiver trabalhando com um conjunto de dados muito grande, a migração de dados pode não ser viável, o que significa que diferentes subconjuntos de seus dados serão armazenados de maneiras diferentes! Uma boa camada de acesso a dados deve ser capaz de abstrair essas diferenças para que o aplicativo não precise ser reescrito toda vez que o esquema evoluir por um motivo ou outro.
Estou apenas construindo outro ODM? Bem, sim, provavelmente. Apenas reluto um pouco em usar o termo porque acho que ele vem acompanhado de alguns dos preconceitos que mencionei aqui. Se for um ODM, será aquele que terá foco no "M. "
E, em parte, só o considero uma coisa gira de construir. É um experimento. Vamos ver se funciona!

Apresentando o DocBridge

Você pode conferir a biblioteca atual no repositório GitHubdo projeto. No momento em que este artigo é escrito, o README contém o que pode ser descrito como um manifesto:
  • Gerenciar grandes quantidades de dados no MongoDB e, ao mesmo tempo, manter um esquema de dados flexível é um desafio.
  • Esse ODM não é uma implementação de registro ativo, mapeando documentos no banco de dados diretamente em objetos similares no código.
  • Esse ODM foi projetado para abstrair documentos subjacentes, mapeando esquemas de documentos potencialmente múltiplos em uma representação de objeto compartilhado. Também deve simplificar a evolução dos documentos no banco de dados, migrando automaticamente os esquemas de documentos individuais na leitura ou na gravação.
  • Deve haver "escotilhas de escape" para que mapeamentos imprevistos possam ser implementados, escondendo o código de implementação por trás de componentes esperançosamente reutilizáveis.

Iniciando uma nova framework

Isso é o suficiente. Vamos começar.
Se você quiser dar uma olhada em como tudo isso funcionará quando tudo se juntar, pule para o final, onde também mostrarei como isso pode ser usado com as queries do PyMongo. Por enquanto, vamos mergulhar de cabeça e começar a implementar uma classe para encapsular documentos BSON para facilitar a abstração de alguns detalhes da estrutura do documento. Em tutoriais posteriores, posso começar a modificar a maneira como as consultas são feitas, mas, no momento, quero apenas encapsular documentos individuais.
Preciso definir classes que encapsulam dados do banco de dados, então vamos chamar essa classe Document. No momento, só preciso que ele armazene um documento "bruto" subjacente, que o PyMongo (e o Motor) fornecem como implementações de dicionário:
1class Document:
2 def __init__(self, doc, *, strict=False):
3 self._doc = doc
4 self._strict = strict
Defina dois parâmetros que são armazenados na instância: doc e strict. O primeiro manterá o documento BSON subjacente para que ele possa ser acessado, e strict é uma bandeira booleana que explicarei abaixo. Neste tutorial, estou ignorando principalmente os detalhes de uso do PyMongo ou Motor para acessar o MongoDB — estou apenas trabalhando com dados de documentos BSON como um antigo dicionário.
Quando uma instância de Documento envolve um documento MongoDB, se strict for False, ele permitirá que qualquer campo no documento seja pesquisado automaticamente como se fosse um atributo Python normal da instância do Documento que o envolve. Se strict for True, ele não permitirá essa pesquisa dinâmica.
Portanto, se eu tiver um documento MongoDB que contenha { 'name': 'Jones' }, envolvê-lo com um documento se comportará assim:
1>>> relaxed_doc = Document({ 'name': 'Jones' })
2>>> relaxed_doc.name
3"Jones"
4
5>>> strict_doc = Document({ 'name': 'Jones' }, strict=True)
6>>> strict_doc.name
7Traceback (most recent call last):
8 File "<stdin>", line 1, in <module>
9 File ".../docbridge/__init__.py", line 33, in __getattr__
10 raise AttributeError(
11AttributeError: 'Document' object has no attribute 'name'
A classe não faz essa pesquisa de atributos mágicos sozinha! Para obter esse comportamento, precisarei implementar __getattr__. Este é um método "mágico" ou "dunder" que é chamado automaticamente pelo Python quando um atributo é solicitado e não está realmente definido na instância ou na classe (ou em qualquer uma das superclasses). Como alternativa, o Python chamará __getattr__ se sua classe o implementar e fornecerá o nome do atributo que foi solicitado.
1def __getattr__(self, attr):
2 if not self._strict:
3 return self._doc[attr]
4 else:
5 raise AttributeError(
6 f"{self.__class__.__name__!r} object has no attribute {attr!r}"
7 )
Isso implementa a lógica que descrevi acima (embora seja um pouco diferente do código no repositório porque havia alguns bugs nele!).
Essa é uma maneira simples de fazer com que um dicionário se pareça com um objeto e permite que os campos do documento sejam pesquisados como se fossem atributos. No entanto, atualmente ele exige que esses nomes de atributos sejam exatamente iguais aos campos subjacentes e só funciona no nível superior do documento. Para tornar o encapsulamento mais eficiente, preciso ser capaz de configurar como os dados são pesquisados por campo. Primeiro, vamos lidar como mapear um atributo para um nome de campo diferente.

Vamos abstrair os nomes dos campos

A primeira abstração que gostaria de implementar é a capacidade de ter um nome de campo diferente no documento BSON daquele exposto pelo objeto Documento. Digamos que eu tenha um documento como este:
1{
2 "cocktailName": "Old Fashioned"
3}
O nome do campo usa camelCase em vez do snake_case mais idiomático (que seria " cocktail_name " em vez de " cocktailName "). Neste ponto, eu poderia alterar o nome do campo com uma consulta do MongoDB, mas isso não é muito sensato (porque não é tão importante) e pode ser controverso com outras equipes que usam o mesmo banco de dados que podem estar mais acostumadas a usar nomes de CamelCase. Então, vamos adicionar a capacidade de mapear explicitamente de um nome de atributo para um nome de campo diferente no documento encapsulado.
Farei isso usando metaprogramação, mas, neste caso, não me exigirá que escreva uma metaclasse personalizada! Vamos presumir que eu vá subclassificar Document para fornecer um mapeamento específico para documentos de receitas de coquetéis.
1class Cocktail(Document):
2 cocktail_name = Field(field_name="cocktailName")
Isso pode parecer semelhante a alguns padrões que você viu usados por outros ODMs ou com, digamos, um modelo Django. No capot, Field precisa implementar o Protocolo do descritor para que possamos interceptar a pesquisa de atributos para cocktail_name em instâncias da classeCocktail e retornar dados contidos no documento BSON subjacente.

O protocolo descritor

O nome parece altamente técnico, mas tudo o que realmente significa é que vou implementar alguns métodos em Field para que o Python possa tratá-lo de forma diferente de duas maneiras diferentes:
__set_name__ é chamado pelo Python quando o campo é anexado a uma classe (neste caso, a classe Cocktail). É chamado, você adivinhou, o nome do campo - neste caso, "Cocktail_name". __get__ é chamado pelo Python sempre que o atributo é pesquisado em uma instância de Cocktail. Então, neste caso, se eu tiver uma instância de Cocktail chamada my_cocktail, o acesso cocktail.cocktail_name a chamará Field.get() sob o capô, fornecendo a instância do campo e a classe à qual o campo está anexado como argumentos. Isso permite que você retorne o que você acha que deve ser retornado por esse acesso de atributo — que é o valor "CocktailName" do documento BSON subjacente.
Aqui está minha implementação de Field. Simplifiquei a implementação no GitHub, mas implementa tudo o que descrevi acima.
1class Field:
2 def __init__(self, field_name=None):
3 """
4 Initialize a Field attribute, mapping to an underlying BSON field.
5
6 field_name is the name of the underlying BSON field.
7 If field_name is None (the default), use the attribute name for lookup in the doc.
8 """
9 self.field_name = None
10
11 def __set_name__(self, owner, name):
12 """
13 Called by Python when this Field instance is attached to a class (the owner).
14 """
15 self.name = name # this is the *attribute* name on the class.
16
17 # If no field_name was provided, then default to using the attribute
18 # name to look up the BSON field:
19 if self.field_name is None:
20 self.field_name = name
21
22 def __get__(self, ob, cls):
23 """
24 Called by Python when this attribute is looked up on an instance of
25 the class it's attached to.
26 """
27 try:
28 # Look up the BSON field and return it:
29 return ob._doc[self.field_name]
30 except KeyError as ke:
31 raise ValueError(
32 f"Attribute {self.name!r} is mapped to missing document property {self.field_name!r}."
33 ) from ke
Com o código acima, implementei um objeto Field, que pode ser anexado a uma classe Document . Ele permite que você permita pesquisas de campo no documento BSON subjacente, com um mapeamento opcional entre o nome do atributo e o nome do campo subjacente.

Vamos abstrair o controle de versão do documento

Um padrão muito comum no MongoDB é o padrão deversionamento de esquema, que é muito importante se você deseja manter a capacidade de desenvolvimento de seus dados. (Este é um termo criado porMartin Kleppmann em seu livro Designing Data Intensive Applications.)
A premissa é que, com o tempo, o esquema do seu documento precisará mudar, seja por motivos de eficiência ou simplesmente porque seus requisitos mudaram. O MongoDB permite armazenar documentos com diferentes estruturas em uma única collection para que uma alteração de esquema não exija que você altere todos os seus documentos de uma só vez — o que pode ser inviável com conjuntos de dados muito grandes de qualquer forma.
Em vez disso, o padrão de versão do esquema sugere que, quando seu esquema for alterado, ao atualizar documentos individuais para a nova estrutura, você atualize um campo que especifica a versão do esquema de cada documento.
Por exemplo, eu poderia começar com um documento representando uma pessoa, como este:
1{
2 "name": "Mark Smith",
3"schema_version": 1,
4}
Mas, eventualmente, talvez eu perceba que preciso separar o nome do usuário:
1{
2 "full_name": "Mark Smith"
3 "first_name": "Mark",
4 "last_name": "Smith",
5 "schema_version": 2,
6}
Neste exemplo, quando carrego um documento desta coleção, não saberei com antecedência se é a versão 1 ou 2, portanto, quando solicito o nome da pessoa, ele pode ser armazenado em "nome" ou "full_name", dependendo se o documento específico foi atualizado ou não.
Para isso, criei um tipo diferente de descritor de "Field", chamado de "FallthroughField". Este receberá uma lista de nomes de campos e tentará procurá-los por sua vez. Dessa forma, posso evitar verificar o campo "schema_version" no documento subjacente, mas ele ainda funcionará com documentos mais antigos e mais recentes.
FallthroughField parece com isto:
1class Fallthrough:
2 def __init__(self, field_names: Sequence[str]) -> None:
3 self.field_names = field_names
4
5 def __get__(self, ob, cls):
6 for field_name in self.field_names: # loop through the field names until one returns a value.
7 try:
8 return ob._doc[field_name]
9 except KeyError:
10 pass
11 else:
12 raise ValueError(
13 f"Attribute {self.name!r} references the field names {', '.join([repr(fn) for fn in self.field_names])} which are not present."
14 )
15
16 def __set_name__(self, owner, name):
17 self.name = name
Obviamente, alterar o nome de um campo é uma mudança de esquema relativamente trivial. Tenho grandes planos sobre como posso usar descritores para abstrair muita complexidade no document model subjacente.

O que parece?

Este tutorial mostra muito código de implementação. Agora, deixe-me mostrar como é usar esta biblioteca na prática:
1import os
2from docbridge import Document, Field, FallthroughField
3from pymongo import MongoClient
4
5collection = (
6 MongoClient(os.environ["MDB_URI"])
7 .get_database("docbridge_test")
8 .get_collection("people")
9)
10
11collection.delete_many({}) # Clean up any leftover documents.
12# Insert a couple of sample documents:
13collection.insert_many(
14 [
15 {
16 "name": "Mark Smith",
17 "schema_version": 1,
18 },
19 {
20 "full_name": "Mark Smith",
21 "first_name": "Mark",
22 "last_name": "Smith",
23 "schema_version": 2,
24 },
25 ]
26)
27
28# Define a mapping for "person" documents:
29class Person(Document):
30 version = Field("schema_version")
31 name = FallthroughField(
32 [
33 "name", # v1
34 "full_name", # v2
35 ]
36 )
37
38# This finds all the documents in the collection, but wraps each BSON document with a Person wrapper:
39people = (Person(doc, None) for doc in collection.find())
40for person in people:
41 print(
42 "Name:",
43 person.name,
44 ) # The name (or full_name) of the underlying document.
45 print(
46 "Document version:",
47 person.version, # The schema_version field of the underlying document.
48 )
Se você executar isso, ele imprimirá o seguinte:
1$ python examples/why/simple_example.py
2Name: Mark Smith
3Document version: 1
4Name: Mark Smith
5Document version: 2

Próximos recursos

Serei o primeiro a admitir que este foi um longo tutorial, visto que, efetivamente, até agora acabei de escrever um wrapper de objeto em torno de um dicionário que pode conduzir um simples remapeamento de nomes. Mas é um ótimo começo para alguns dos recursos mais avançados que estão por vir:
  • A capacidade de atualizar automaticamente os dados em um documento quando os dados são calculados ou gravados de volta no banco de dados
  • Definições de classe recursivas para garantir que você tenha todo o poder da estrutura, independentemente do grau de aninhamento dos seus dados
  • A capacidade de lidar de forma transparente com o subconjunto e padrõesde referência estendidos para carregar dados lentamente de documentos e collection
  • Remapeamento de nome mais avançado para criar objetos Python que se parecem com objetos Python, em documentos que podem ter convenções drasticamente diferentes
  • Potencialmente algumas ferramentas para ajudar a criar queries complexas em relação aos seus dados
Mas a próxima coisa a fazer é dar um passo para trás na escrita de código de biblioteca e fazer algumas tarefas domésticas. Estou construindo uma estrutura de teste para ajudar a testar diretamente no MongoDB e, ao mesmo tempo, ter minhas gravações de teste revertidas após cada teste, e estou começando a pacote e publicar a biblioteca docbridge. Você pode conferir a gravação da transmissão ao vivo onde tento fazer isso, ou pode aguardar o tutorial que o acompanha, que será escrito a qualquer momento.
Estou transmitindo no canal do MongoDB no YouTube quase todas as terças-feiras, às 2 pm GMT! Junte-se a nós — é sempre útil ter mais pessoas identificando os bugs que estou criando enquanto escreva o código!

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

MongoDB.Live 2020 Keynote em menos de 10 minutos


Mar 21, 2023 | 1 min read
exemplo de código

Gestão de revistas


Sep 11, 2024 | 0 min read
Início rápido

Introdução às transações ACID multidocumento em Python


Sep 11, 2024 | 10 min read
Artigo

Um resumo dos antipadrões de projeto de esquema e como identificá-los


Oct 01, 2024 | 3 min read
Sumário