Simple registering of Tortoise ORM models with FastAPI

8.5k views Asked by At

I have a FastAPI app with an existing MySQL database, and I'm trying to use Tortoise ORM. The (simplified) main FastAPI file looks like this:

from fastapi import FastAPI
import os
from tortoise.contrib.fastapi import register_tortoise

# Register FastAPI main app
app = FastAPI(title="my_app")


# Database
DATABASE_URL = "mysql://{}:{}@{}:{}/{}".format(
    os.environ["MYSQL_USER"],
    os.environ["MYSQL_PASSWORD"],
    os.environ.get("MYSQL_HOST", "127.0.0.1"),
    os.environ.get("MYSQL_PORT", "3306"),
    os.environ.get("MYSQL_DB", "my_db"),
)

# Register Tortoise ORM with DB
register_tortoise(
    app,
    db_url=DATABASE_URL,
    modules={"models": ["models"]},
    generate_schemas=False,
    add_exception_handlers=True,
)


# Test SQL query
from models import Store
print(Store.get(api_key="api_key"))

...and a models.py file, at same base directory level, looking like this:

from tortoise import fields
from tortoise.models import Model


class Store(Model):
    api_key = fields.CharField(max_length=64, db_index=True)
    name = fields.CharField(max_length=255)

    def __str__(self):
        return self.name

    class Meta:
        table = "stores"

However, I get an error from Tortoise ORM:

  File ".../site-packages/tortoise/models.py", line 265, in db
    raise ConfigurationError("No DB associated to model")
tortoise.exceptions.ConfigurationError: No DB associated to model

Any idea why?

I'm following the doc (https://tortoise-orm.readthedocs.io/en/latest/contrib/fastapi.html) but the path/syntax for "modules that should be discovered for models" is not very clear to me. I also tried with registering the models with pydantic_model_creator, though not clear in the doc why you need that (https://tortoise-orm.readthedocs.io/en/latest/examples/fastapi.html#example-fastapi). I would prefer not to use the config.json full config file loaded by register_tortoise, it seems optional according to the doc.

2

There are 2 answers

3
bolino On BEST ANSWER

The problem was coming from the async nature of FastAPI and Tortoise ORM. We have to wait for FastAPI to load and Tortoise to register the models.

So we can successfully do an async request that waits for FastAPI to load, and Tortoise ORM to request, like this:

# Test SQL query
from models import Store

@app.on_event("startup")
async def startup():
    print(await Store.get(api_key="api_key"))
0
Anwar Husain On

In our case, one of the ASGI middleware (which did not support asgi lifespan events) was raising an error during startup which prevented the ASGI lifespan events (namely startup) from firing and did not register the models. We ended up patching the middleware to be fired only for scope['type'] == 'http'.

The ASGI spec states that the server must continue even after startup errors.

If an exception is raised when calling the application callable with a lifespan.startup message or a scope with type lifespan, the server must continue but not send any lifespan events.

However tortoise ORM register_tortoise function relies on the lifespan event startup to register models. So I think the lifespan mode on uvicorn should be on instead of the default auto. This way your server process will terminate rather than serving a misconfigured app.

uvicorn.run("start_server:app", host="0.0.0.0", port=8080, log_level="info", lifespan='on')