Python Friday #233: SQLAlchemy and FastAPI

Until now we kept our data in a variable. While that worked with an example application, the data vanishes as soon as we restart our API. To get a more realistic application, we need to persist data for a longer time. Let us explore how we can integrate SQLAlchemy with FastAPI.

This post is part of my journey to learn Python. You find the code for this post in my PythonFriday repository on GitHub.

 

The extended to-do application

The current state of the to-do application is a good basic example for FastAPI. I copied the application as it is after the introduction of the router into a new folder named extended_todo. Here we can add the new features for the rest of this series on FastAPI.

 

Install SQLAlchemy

I covered SQLAlchemy in great detail a few years back. Since then, version 2 was released, and we need a few changes in our examples. We can install (or update) SQLAlchemy with this command:

 

How can we integrate SQLAlchemy and FastAPI?

When it comes to the integration of SQLAlchemy in FastAPI, we get a lot of flexibility. For the models and entities, we can choose between these 2 options:

  1. Dedicated models for Pydantic and SQLAlchemy.
  2. One combined model for Pydantic and SQLAlchemy (for example with SQLModel)

When we know how to split the models, we need to decide how we want to access the database:

  1. Through a repository that hides the SQLAlchemy commands from our application.
  2. Our models interact directly with the database (like the Active record pattern).
  3. SQLAlchemy commands in our API endpoints.

All options have their pros and cons, so it is up to the specific application to make the trade-off.

I like the in-memory data store I currently have with its separation of concerns and the two Pydantic models to split the input from the output. I will continue with this idea and create a separate entity Task that is a SQLAlchemy entity that maps to the table. My datastore will continue to use the Pydantic models as input and output but works with the Task entity behind the scenes.

 

The SQLAlchemy configuration

We need to tell SQLAlchemy what kind of database we want to use and create some setup code to wire everything together. For that we add the database.py file inside the data folder with this code:

The create_session_factory() method sets everything up and creates a session factory for us, that we will use as our entry into the database. This is similar to the official tutorial, but I prefer to set the file name for the database from the outside.

 

The database models

To create our classes that match our tables, we need a base class that creates the SQLAlchemy magic behind the scenes. We can put that code into entitybase.py in the data folder:

For our Task entity (the class that matches the table), we can create the file entities.py in the data folder and put it there:

As a final step we create the file data/__all_models.py to have one place that contains all our entities – that way SQLAlchemy can create them when it initialises the database:

 

The tests for the new data store

We continue our test first approach and start with the tests for the database store we want to use with SQLAlchemy. As always, start with a test, implement enough code to make it work and then repeat. To shorten this post, here are all the tests that we need:

The Pytest fixture with_db() configures the database using our above created factory.

Make sure that you have a db folder next to the data and test folders.

 

Implementing the data store

We can use our tests and implement the new DataStoreDb class inside data/datastore_db.py:

The private method __to_output() turns our Task into a TaskOutput. The different data types of created_at are there by design so that we can see how we can create a converter to map from the database objects to the ones we use in FastAPI.

With this code in place, we can run our tests. Everything should pass, including the old tests.

 

Use the data store in the FastAPI application

Since we moved the /todo endpoints into a router file, we need to open routers/todo.py, wire up the database and replace the datastore:

We need to change all endpoint methods and add the dependency for our get_db() method:

The rest of the code stays the same. Even better, our tests for our endpoints need not to change at all.

 

Next

We moved from the in-memory data store to SQLAlchemy, and our API works as before. We have the tests to check that this is not only a claim, but reality.

However, there is something that could be a problem. Our endpoint tests write into the database we defined in the router and not into the same place as the datastore tests. Next week we optimise the database tests and fix that problem.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.