You have no excuses now, use this free credit to launch your projects now on Digital Ocean, you're free to spend it whenever you want within the following 60 days.
Table of contents
Python tortoise ORM integration with FastAPI
Python tortoise ORM integration with FastAPI
One of the things I like most about Django is its ORM; one of the reasons why this framework is so popular . On the other hand FastAPI does not have an ORM and focuses solely on serving endpoints, showing agnostic on the basis of data. There are enough options ORM to python: django-alchemy, peewee, ponyORM, tortoise. The latter, besides being asynchronous, is inspired by the django ORM, so its syntax is quite similar, even many tortoise functions share name with its Django counterpart, so users who use the Django ORM will save a lot of time learning tortoise functions.
For this tutorial I’m going to use fastAPI and tortoise-orm together so make sure you know at least the basics of the fastAPI framework and database basics.
Tortoise compatibility
Tortoise is compatible with the following databases.
- PostgreSQL >= 9.4 (using asyncpg)
- SQLite (using aiosqlite)
- MySQL/MariaDB (using aiomysql or asyncmy)
But for this example I am going to use SQLite, because it does not need any kind of configuration.
Installation of the Python tortoise ORM
To install tortoise-orm just use the virtual environment manager of your choice, I will use pipenv.
pip install tortoise-orm
I will also install fastAPI and other utilities we will need
pip install python-multipart fastapi uvicorn pydantic
Create models with tortoise
Let’s create a directory called app and a models file called models.py.
# app/models.py
from tortoise.models import Model
from tortoise import fields
class Job(Model):
# El campo de la llave primaria se crea automáticamente
# id = fields.IntField(pk=True)
name = fields.CharField(max_length=255)
description = fields.TextField()
def __str__(self):
return self.name
If you notice the syntax is quite similar to the Django fields, even some parameters are the same.
To start working with the tortoise ORM we need:
- Connect to the database.
- Create the necessary table(s).
Connecting to database with tortoise
We are going to create a function to connect to the database in a directory called database:
# database/connectToDatabase.py
from tortoise import Tortoise
async def connectToDatabase():
await Tortoise.init(
db_url='sqlite://db.sqlite3',
modules={'models': ['app.models']}
)
Generating schematics with tortoise
Now let’s create a function to generate the models in the root of our application.
# createSchema.py
from tortoise import Tortoise, run_async
from database.connectToDatabase import connectToDatabase
async def main():
await connectToDatabase()
await Tortoise.generate_schemas()
if __name__ == '__main__':
run_async(main())
Observe how we import the function to connect that we have just created and then call the generate_schemas() method, which will read our models and make the changes in the database.
Another aspect you should appreciate is that we run the main function inside the run_async() function provided by tortoise. This is necessary for our await functions to run, otherwise only a corroutine object would be created.
Why do we place this method in an external file? Because generate_schemas() only needs to be used once; when the tables are created. We should not include it in the file that will be run when fastAPI is executed.
Knowing that, let’s run it to create our tables.
python3 createSchema.py
If everything went well we will already have the tables created in our SQLite database.
tortoise integration with fastAPI
We will start with a simple fastAPI application.
To connect fastAPI with tortoise, the latter gives us a function called register_tortosise(). That receives the instance we created with fastAPI, the address to the database and the location of our models.
# main.py
from fastapi import FastAPI
from database.connectToDatabase import connectToDatabase
app = FastAPI()
await connectToDatabase()
@app.get("/")
async def read_root():
return {"Hello": "World"}
register_tortoise(
app,
db_url="sqlite://db.sqlite3",
modules={"models": ["app.models"]},
generate_schemas=True,
add_exception_handlers=True,
)
Create an object with tortoise
To create an object we can choose to call the create() method of the model, inside a function decorated with the post() method of our instance, or also create an instance and then call its save() method.
# main.py
from fastapi import FastAPI
from tortoise.contrib.fastapi import HTTPNotFoundError, register_tortoise
app = FastAPI()
@app.get("/")
async def read_root():
return {"Hello": "World"}
@app.post("/job/create/", status_code=201)
async def create_job(name=Form(...), description=Form(...)):
job = await Job.create(name=name, description=description)
return {"status":"ok"}
# ...
If we now make a web request using the documentation interface that fastAPI creates, in /docs/, we will see that we will be able to create a Job object using a name and a description.
Serializing objects with pydantic and tortoise
We have created the object, but what if we want to return the object after creating it? Since it is an instance of a model, we can’t just return it like that. We need a data type suitable for an HTTP response.
Pydantic allows us to serialize database objects in order to return them as JSON response or whatever we want.
We must import the pydantic_model_creator function and pass it our model as a parameter.
# main.py
from fastapi import FastAPI
from app.models import Job
from tortoise.contrib.fastapi import HTTPNotFoundError, register_tortoise
from tortoise.contrib.pydantic import pydantic_model_creator
app = FastAPI()
job_pydantic = pydantic_model_creator(Job)
@app.get("/")
async def read_root():
return {"Hello": "World"}
@app.post("/job/create/", status_code=201)
async def create_job(name=Form(...), description=Form(...)):
job = await Job.create(name=name, description=description)
return await job_pydantic.from_tortoise_orm(job)
register_tortoise(
app,
db_url="sqlite://db.sqlite3",
modules={"models": ["app.models"]},
add_exception_handlers=True,
)
And to get our object in JSON we call the method to the from_tortoise_orm() method of the object we just created.
Remember to prefix the word await or what you will return is a corroutine.
Obtain a list of objects from a queryset
We will use the get() method of our fastAPI instance.
To obtain a list of objects we use the all() method and serialize the result with from_queryset().
# main.py
# ...
@app.get("/jobs/")
async def get_jobs():
return await job_pydantic.from_queryset(Job.all())
Updating an object with tortoise
Now create an endpoint that receives an id and is decorated with the put() method. We pass as response_model the job_pydantic object, to validate the data input, include it in the documentation and limit the response to modifiable fields .
We will also create a second job_pydantic object, i.e. another serializer, which excludes the read-only fields (our primary key), to return them without id.
And, to update an object, we use the fastAPI put method and receive the id of the object to edit. Then we filter those objects that match the id with Job.filter() and then call its update() method. Since the id is unique, as it is a primary key, only the object whose id matches the data we send will be edited.
# main.py
job_pydantic = pydantic_model_creator(Job)
job_pydantic_no_ids = pydantic_model_creator(Job, exclude_readonly=True)
# ...
@app.put("/job/{job_id}", response_model=job_pydantic, responses={404: {"model": HTTPNotFoundError}})
async def update_job(job_id: int, job: job_pydantic):
await Job.filter(id=job_id).update(**job.dict())
return await job_pydantic_no_ids.from_queryset_single(Job.get(id=job_id))
Obtain an object with tortoise
Now we can apply the same method as in the previous section. This time we will need an id and the fastAPI’s get() method. We pass it the response_model to take care of the validation and define that the only parameter we will use will be the id, with which we will use the from_queryset_single() method on the result of the ORM query: Job.ge_t(id=job_id)_.
# main.py
# ...
@app.get("/job/{job_id}", response_model=job_pydantic, responses={404: {"model": HTTPNotFoundError}})
async def get_job(job_id: int):
return await job_pydantic_no_ids.from_queryset_single(Job.get(id=job_id))
Remove an object with tortoise
To delete an object we will also need an id and call fastAPI’s delete() method, so the function would look like this:
# main.py
class Status(BaseModel):
message: str
# ...
@app.delete("/job/{job_id}", response_model=Status, responses={404: {"model": HTTPNotFoundError}})
async def delete_job(job_id: int):
deleted_job = await Job.filter(id=job_id).delete()
if not deleted_job:
raise HTTPException(status_code=404, detail=f"Job {job_id} not found")
return Status(message=f"Deleted job {job_id}")
We filter by the id we get in the url and, if we find the object, we delete it, in case the id of that object does not exist we will return a 404 error through an exception. In case it does, we will no longer return the object, but it will be enough that we return a message warning that the id was deleted.
And with that we can perform basic CRUD operations in fastAPI using tortoise as ORM. In this entry I haven’t discussed foreign keys, foreign key fields, many to many, or other kinds of relationships between models. I will probably make a future post about that, in the meantime you can read the official tortoise documentation