Introdução ao MongoDB e FastAPI
Aaron Bassett, Mark Smith7 min read • Published Feb 05, 2022 • Updated Jul 12, 2024
Avalie esse Início rápido
A FastAPI é um framework Python 3.6+ moderno, de alto desempenho, fácil de aprender, rápido de codificar e pronto para produção. É usado para criar APIs com base nas dicas padrão do tipo Python. Embora possa não estar tão estabelecido quanto alguns outros frameworks Python, como Django, ele já está na produção em empresas como Uber, Netflix e Microsoft.
A FastAPI é assíncrona e, como o próprio nome indica, é super rápida; então, o MongoDB é o acompanhamento ideal. Neste início rápido, criaremos um aplicativo CRUD (Criar, Ler, Atualizar, Excluir) mostrando como você pode integrar o MongoDB com seus projetos FastAPI.
- Python 3.9.0
- Um cluster do MongoDB Atlas. Siga o guia "Introdução ao Atlas" para criar sua conta e o cluster do MongoDB. Anote seu nome de usuário, senha e string, pois você precisará deles mais tarde.
1 git clone git@github.com:mongodb-developer/mongodb-with-fastapi.git
Você precisará instalar algumas dependências: FastAPI, Motor etc. Sempre recomendo que você instale todas as dependências do Python em um virtualenv para o projeto. Antes de executar o pip, verifique se o virtualenv está ativo.
1 cd mongodb-with-fastapi 2 pip install -r requirements.txt
Pode levar alguns minutos para baixar e instalar suas dependências. Isso é normal, especialmente se você nunca instalou um pacote.
Depois de instalar as dependências, você precisará criar uma variável de ambiente para sua string de conexão do MongoDB.
1 export MONGODB_URL="mongodb+srv://<username>:<password>@<url>/<db>?retryWrites=true&w=majority"
Lembre-se, sempre que você iniciar uma nova sessão de terminal, precisará definir essa variável de ambiente novamente. Eu uso direnv para tornar este processo mais fácil.
A etapa final é iniciar seu servidor FastAPI.
1 uvicorn app:app --reload
Quando tiver tido a chance de experimentar o exemplo, volte e analisaremos o código.
Todo o código do aplicativo de exemplo está dentr deo
app.py
. Vamos dividi-lo em seções e mostrar o que cada uma está fazendo.Uma das primeiras coisas que fazemos é nos conectar ao nosso MongoDB database.
1 client = motor.motor_asyncio.AsyncIOMotorClient(os.environ["MONGODB_URL"]) 2 db = client.get_database("college") 3 student_collection = db.get_collection("students")
Estamos usando o driver de motor assíncrono para criar nosso cliente MongoDB e, em seguida, especificamos o nome do nosso banco de dados
college
.1 # Represents an ObjectId field in the database. 2 # It will be represented as a `str` on the model so that it can be serialized to JSON. 3 PyObjectId = Annotated[str, BeforeValidator(str)]
O MongoDB armazena dados como BSON. A FastAPI codifica e decodifica dados como strings JSON. BSON tem suporte para outros tipos de dados não nativos JSON, incluindo
ObjectId
que não podem ser codificados diretamente como JSON. Por isso, convertemos ObjectId
s em strings antes de armazená-las como o campo id
.Muitas pessoas pensam que o MongoDB não tem esquema, o que está errado. O MongoDB tem um esquema flexível. Ou seja, as coleções não impõem uma estrutura de documento por padrão, então você tem a flexibilidade de usar quaisquer opções de modelagem de dados que melhor correspondam ao seu aplicativo e aos requisitos de desempenho. Portanto, é comum criar modelos ao trabalhar com um MongoDB database. Nosso aplicativo tem três modelos, o
StudentModel
, o UpdateStudentModel
e o StudentCollection
.1 class StudentModel(BaseModel): 2 """ 3 Container for a single student record. 4 """ 5 6 # The primary key for the StudentModel, stored as a `str` on the instance. 7 # This will be aliased to `_id` when sent to MongoDB, 8 # but provided as `id` in the API requests and responses. 9 id: Optional[PyObjectId] = Field(alias="_id", default=None) 10 name: str = Field(...) 11 email: EmailStr = Field(...) 12 course: str = Field(...) 13 gpa: float = Field(..., le=4.0) 14 model_config = ConfigDict( 15 populate_by_name=True, 16 arbitrary_types_allowed=True, 17 json_schema_extra={ 18 "example": { 19 "name": "Jane Doe", 20 "email": "jdoe@example.com", 21 "course": "Experiments, Science, and Fashion in Nanophotonics", 22 "gpa": 3.0, 23 } 24 }, 25 )
Quero chamar a atenção para o campo
id
neste modelo. O MongoDB usa _id
, mas no Python, os sublinhados no início dos atributos têm um significado especial. Se você tiver um atributo em seu modelo que comece com um sublinhado, pydantic–a estrutura de validação de dados usada pelo FastAPI–assumirá que é uma variável privada, o que significa que você não poderá atribuir um valor a ele! Para contornar isso, nomeamos o campo id
, mas damos a ele um alias de _id
. Você também precisa definir populate_by_name
como True
no model_config
Definimos esse valor
id
automaticamente como None
, portanto, você não precisa fornecê-lo ao criar um novo aluno.1 class UpdateStudentModel(BaseModel): 2 """ 3 A set of optional updates to be made to a document in the database. 4 """ 5 6 name: Optional[str] = None 7 email: Optional[EmailStr] = None 8 course: Optional[str] = None 9 gpa: Optional[float] = None 10 model_config = ConfigDict( 11 arbitrary_types_allowed=True, 12 json_encoders={ObjectId: str}, 13 json_schema_extra={ 14 "example": { 15 "name": "Jane Doe", 16 "email": "jdoe@example.com", 17 "course": "Experiments, Science, and Fashion in Nanophotonics", 18 "gpa": 3.0, 19 } 20 }, 21 )
O
UpdateStudentModel
tem duas diferenças importantes em relação ao StudentModel
:- Ele não tem um atributo
id
, pois isso não pode ser modificado. - Todos os campos são opcionais, então você só precisa fornecer os campos que deseja atualizar.
Por fim,
StudentCollection
é definido para encapsular uma lista de instâncias StudentModel
. Em teoria, o endpoint poderia retornar uma lista de nível superior de StudentModels, mas há algumas vulnerabilidades associadas ao retorno de respostas JSON com listas de nível superior.1 class StudentCollection(BaseModel): 2 """ 3 A container holding a list of `StudentModel` instances. 4 5 This exists because providing a top-level array in a JSON response can be a [vulnerability](https://haacked.com/archive/2009/06/25/json-hijacking.aspx/) 6 """ 7 8 students: List[StudentModel]
Nosso aplicativo tem cinco rotas:
- POST /students/ - cria um novo aluno.
- GET /students/ - ver uma lista de todos os alunos.
- GET /students/{id} - ver um único aluno.
- PUT /students/{id} - atualizar um aluno.
- DELETE /students/{id} - exclui um aluno.
1 2 3 4 5 6 7 8 async def create_student(student: StudentModel = Body(...)): 9 """ 10 Insert a new student record. 11 12 A unique `id` will be created and provided in the response. 13 """ 14 new_student = await student_collection.insert_one( 15 student.model_dump(by_alias=True, exclude=["id"]) 16 ) 17 created_student = await student_collection.find_one( 18 {"_id": new_student.inserted_id} 19 ) 20 return created_student
A rota
create_student
recebe os dados do novo aluno como uma string JSON em uma solicitaçãoPOST
. Temos que decodificar esse corpo da solicitação JSON em um dicionário Python antes de passá-lo ao nosso cliente MongoDB.A resposta do método
insert_one
inclui o _id
do aluno recém-criado (fornecido como id
) porque esse ponto de extremidade especifica response_model_by_alias=False
na chamada do decorador post
. Depois de inserir o aluno em nossa coleção, usamos o inserted_id
para encontrar o documento correto e devolvê-lo em nosso JSONResponse
.A FastAPI retorna um código de status HTTP
200
por padrão; mas neste caso, um 201
criado é mais apropriado.O aplicativo tem duas rotas de leitura: uma para visualizar todos os alunos e outra para visualizar um aluno individual.
1 2 3 4 5 6 7 async def list_students(): 8 """ 9 List all of the student data in the database. 10 11 The response is unpaginated and limited to 1000 results. 12 """ 13 return StudentCollection(students=await student_collection.find().to_list(1000))
O método
to_list
do Motor requer um argumento de contagem máxima de documentos. Para este exemplo, codifiquei-o para 1000
, mas em um aplicativo real, você usaria os parâmetros skip e limit em find
para paginar seus resultados.1 2 3 4 5 6 7 async def show_student(id: str): 8 """ 9 Get the record for a specific student, looked up by `id`. 10 """ 11 if ( 12 student := await student_collection.find_one({"_id": ObjectId(id)}) 13 ) is not None: 14 return student 15 16 raise HTTPException(status_code=404, detail=f"Student {id} not found")
A rota de detalhes do aluno tem um parâmetro de caminho
id
, que a FastAPI passa como argumento para a função show_student
. Usamos id
para tentar encontrar o aluno correspondente no banco de dados. A condicional nesta seção está usando uma expressão de atribuição, um acréscimo ao Python 3.8 e muitas vezes referida pelo apelido "operador walrus."Se um documento com o
_id
especificado não existir, criaremos uma HTTPException
com status de 404
.1 2 3 4 5 6 7 async def update_student(id: str, student: UpdateStudentModel = Body(...)): 8 """ 9 Update individual fields of an existing student record. 10 11 Only the provided fields will be updated. 12 Any missing or `null` fields will be ignored. 13 """ 14 student = { 15 k: v for k, v in student.model_dump(by_alias=True).items() if v is not None 16 } 17 18 if len(student) >= 1: 19 update_result = await student_collection.find_one_and_update( 20 {"_id": ObjectId(id)}, 21 {"$set": student}, 22 return_document=ReturnDocument.AFTER, 23 ) 24 if update_result is not None: 25 return update_result 26 else: 27 raise HTTPException(status_code=404, detail=f"Student {id} not found") 28 29 # The update is empty, but we should still return the matching document: 30 if (existing_student := await student_collection.find_one({"_id": id})) is not None: 31 return existing_student 32 33 raise HTTPException(status_code=404, detail=f"Student {id} not found")
A rota
update_student
é como uma combinação das rotascreate_student
e show_student
. Ela recebe o id
do documento a ser atualizado, bem como os novos dados no corpo do JSON. Não queremos atualizar nenhum campo com valores vazios; portanto, em primeiro lugar, iteramos todos os itens no dicionário recebido e adicionamos apenas os itens que têm um valor ao nosso novo documento.Se, depois de removermos os valores vazios, não houver campos para atualizar, procuramos um registro existente que corresponda a
id
e o retornamos inalterado. No entanto, se houver valores a serem atualizados, usamos find_one_and_update para $set os novos valores e, em seguida, retornamos o documento atualizado.Se chegarmos ao final da função e não conseguirmos encontrar um documento correspondente para atualizar ou retornar, criaremos um erro
404
novamente.1 2 async def delete_student(id: str): 3 """ 4 Remove a single student record from the database. 5 """ 6 delete_result = await student_collection.delete_one({"_id": ObjectId(id)}) 7 8 if delete_result.deleted_count == 1: 9 return Response(status_code=status.HTTP_204_NO_CONTENT) 10 11 raise HTTPException(status_code=404, detail=f"Student {id} not found")
Nossa rota final é
delete_student
. Novamente, como isso está afetando um único documento, temos que fornecer um id
no URL. Se encontrarmos um documento correspondente e o excluirmos com êxito, retornaremos um status HTTP de 204
ou "Sem conteúdo". Nesse caso, não devolvemos um documento porque já o excluímos! No entanto, se não conseguirmos encontrar um aluno com o id
especificado, retornamos um 404
.Se você estiver ansioso para criar algo mais pronto para produção com FastAPI, React e MongoDB, acesse o repositório do Github para encontrar nosso novo gerador de aplicativos FastAPI e comece a transformar sua experiência de desenvolvimento web.
Espero que você tenha gostado dessa introdução à FastAPI com MongoDB. Se quiser saber mais, confira minha publicação sobre como introduzir a pilha FARM (FastAPI, React e MongoDB), bem como a documentação da FastAPI e esta lista bacana.
Se tiver dúvidas, acesse o website da comunidade de desenvolvedores, onde os engenheiros do MongoDB e da MongoDB Community ajudarão a criar sua próxima grande ideia com o MongoDB.
Relacionado
Tutorial
Impulsionando a IA: construa um chatbot sobre seus dados com o MongoDB Atlas Vector Search e os modelos LangChain usando o padrão RAG
Sep 18, 2024 | 7 min read