
FastAPI for Django Programmers: An ORM
Django’s ORM is one of its best features. It might not be the fastest, and has a number of issues, but it’s easy to use and fits well into the framework. FastAPI, like many frameworks, doesn’t come with an ORM, so I went on the hunt for one.
My first stop, like most of us, was SQLAlchemy. I’ve been down this road a number of times, and no matter what, I just don’t like the library. There just seems to be too many ways of doing the same thing. Tutorials and online docs never seem to match each other and offer up differing parameters to be used, as defaults.
I am not saying this is a bad system or bad ORM, it’s just not for me. Maybe its too distant from Django’s ORM.
My search moved on. There are a couple of good ORM’s out there, some with some unique prospective, but as soon as a saw Tortoise ORM, I stopped. I found my ORM home and I was not going to move on.
Tortoise ORM
Lets take a look at this little gem. First off, its “inspired by Django’s ORM”. As you will see later, the look and feel of Tortoise will feel very familiar to Django programmers. A BIG plus.
Next the ORM is async. That fits right into the whole FastAPI Async route, but this does mean that the ORM in a lot of places, does not match or work like Django’s ORM. This is not such a bad thing. As a Django programmer, I am well used to having to think about the number of DB calls I’m making and optimizing them, as Django threads slow down quickly, if there are too many. With Tortoise, you need to be aware of the calls to the DB, as each call will need to be awaited, as they are coroutines.
The last main feature I will add here is that Tortoise comes with a FastAPI interface class, so adding it in is really easy.
Note. No File fields. Tortoise has a number of additional field types, but it doesn’t come with any file management, so no file, or image fields. I don’t know what its like for other Django Programmers, but my projects don’t make a huge use of these fields, so me its not much of a loss.
Note. Migration is a separate package. I remember the bad old days of using South, and the first few releases of Django’s migration system, so I view this with some trepidation. Then again, most ORM’s out there work they same way, so…
Note. I have recently tried aerich, the migration package and it didn’t work :( I would love to say that its great, but it failed on the initial set-up steps, so I didn’t even get to doing migrations. I have found manual DB changes are a lot easier when you have an AI assistant who can feed you the SQL commands though :)
Tortoise settings for FastAPI
As I said Tortoise comes with its own class or FastAPI integration. In the example below, I’ve used python-dotenv, along with Tortoise in the main.py file. I would be surprised if any of you have not used dotenv or similar to secure your deployments. If you haven’t, well, you kinda should be.
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from tortoise.contrib.fastapi import register_tortoise
from dotenv import find_dotenv, dotenv_values
from app1.views import app1_views
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
settings = dotenv_values(find_dotenv(".settings"))
secret_key = settings.get("secret_key")
db_user = settings.get("db_user")
db_password = settings.get("db_password")
db_host = settings.get("db_host")
db_port = settings.get("db_port")
db_name = settings.get("db_name")
register_tortoise(
app,
db_url=f"asyncpg://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}",
modules={"models": ["models"]},
generate_schemas=True,
add_exception_handlers=True,
)
app.include_router(app1_views)For my database, I went with Postgresql since its the one that I’ve used the most (django’s fav), but I’ve used the async driver asyncpg instead of psycopg2. generate_schemas results in any tables in models.py being created, if they are not in the DB already. You can also see that you can add a list of models files, not just one. They should be put in as python modules, so app1.models, app2.models etc if you want more than one. I’ve placed my models file in the root and just called it models.py.
My main.py file, now looks more like a proper setting file, so you can see why I suggested not placing views in this file.
Models
Coding Models is not going to tax your brain. Its the section of Tortoise that is most like Django, I would even say the format is somewhat simpler.
from tortoise import fields, models
class Tokens(models.Model):
id = fields.IntField(pk=True)
token = fields.CharField(max_length=30)
age = fields.DatetimeField()
class Users(models.Model):
id = fields.IntField(pk=True)
username = fields.CharField(max_length=50)
password = fields.CharField(max_length=512)
email = fields.CharField(max_length=300)
last_login = fields.DateField()
additional = fields.JSONField(null=True)
class Session(models.Model):
id = fields.IntField(pk=True)
user = fields.ForeignKeyField("models.Users", related_name="session_user")
data = fields.BinaryField()
expires = fields.IntField()
token = fields.CharField(max_length=50)
class Project(models.Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=200)
code = fields.JSONField(null=True)
template = fields.BooleanField(default=False)
version = fields.IntField(default=0)
class ProjectSection(models.Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=100)
project = fields.ForeignKeyField("models.Project", related_name="project_section")
code = fields.JSONField(nul=True)Model Fields
I’ve already mentioned that Tortoise does not have any File fields, so that the first item to be aware of.
The second is that there is no choices parameter, or any params that would be used in an Admin panel. Choices has been replaced by a set of Enum based fields. I think using Enum instead of choices is a good thing, but that's a personal choice.
You can also state the id field in the model. If you don’t do this, then Tortoise adds one for you, but, again, I found l liked this feature. Most of other fields work about the same as Django equivalent ones.
There is one option that I’ve seen and not used. You can define your own validators for data being placed in a field. I like the option of having that.
Queries
Queries are where things start to get a little different. The syntax is a little simpler than Django’s, but queries over all, are a little more complex.
The first thing to realize is that all Queries to the database have to be awaited and sometimes its not obvious where a query is being made. For example, you send a query that will return a QuerySet. This of course will be awaited. You then want to read the first record in the QuerySet. Again, this will need to be awaited while it gets the record. Next you want to read a related record to this one, so again you need to await this while the new related record is fetched. Yep, lots of awaiting.
You need to be VERY aware of where your getting QuerySets and where your getting records.
Prefetching
I think most Django programmers have gotten used to pulling data using values and values_list. These are both available in Tortoise. However Tortoise also has fetch_related for QuerySingleSet and prefetch_related for QuerySet’s. These can be used to grab related records at the same time as you grab the initial ones. Its a BIG topic, so I suggest going through the docs on this. For me, I like to stick with values and values_list.
Iterating over a QuerySet
As a last item, I thought I would just show how you would write code to iterate over a queryset. I think you can see that this would be different, as each iteration would need to be awaited some how.
from tortoise import Tortoise, fields, run_async, Model
from tortoise.query_utils import Q
class SampleModel(Model):
id = fields.IntField(pk=True)
name = fields.TextField()
async def iterate_results():
await Tortoise.init(
db_url='postgres://localhost:5432/your_database',
modules={'models': ['__main__']}
)
queryset = SampleModel.filter(Q(name="name1") | Q(name="name2"))
async for item in queryset:
print(item.id, item.name)
run_async(iterate_results())Here you can see the async keyword is used with the for loop. Just something I found buried in the docs.
Conclusion
I could spend hours going over all the features and uses of Tortoise, but at its heart, it feels comfortable for me to use as a Django programmer, which cannot be said for all ORM’s. It has all the ORM features and a number of very advanced ones, which I think, will cover most, if not all usecases. The most important features after the Django feel to it, is that it works well with FastAPI, and I don’t have to twist the libraries into doing something they are not designed to do.
References
Tortoise HomePage
SQLAlchemy Documentation
