Tortoise ORM Integration¶
Unfazed uses Tortoise ORM as its database layer and Aerich for schema migrations. Database setup is fully automatic — provide a DATABASE configuration in settings and Unfazed initialises the ORM, discovers models from installed apps, and registers migration commands. If no DATABASE is configured, the ORM layer is skipped entirely.
Quick Start¶
1. Configure the database¶
# entry/settings/__init__.py
UNFAZED_SETTINGS = {
...
"INSTALLED_APPS": [
"apps.blog",
],
"DATABASE": {
"CONNECTIONS": {
"default": {
"ENGINE": "tortoise.backends.mysql",
"CREDENTIALS": {
"HOST": "localhost",
"PORT": 3306,
"USER": "root",
"PASSWORD": "secret",
"DATABASE": "mydb",
},
}
}
},
}
2. Define models in an installed app¶
Create a models.py in your app package. Unfazed discovers it automatically:
# apps/blog/models.py
from tortoise import Model, fields
class Post(Model):
title = fields.CharField(max_length=255)
body = fields.TextField()
published = fields.BooleanField(default=False)
created_at = fields.DatetimeField(auto_now_add=True)
class Meta:
table = "blog_post"
3. Run migrations¶
# Initialise the migration directory (first time only)
unfazed-cli init-db
# Generate a migration after model changes
unfazed-cli migrate --name add_post
# Apply pending migrations
unfazed-cli upgrade
Database Configuration¶
The DATABASE setting is a dict validated by the Database Pydantic model:
UNFAZED_SETTINGS = {
...
"DATABASE": {
"CONNECTIONS": {
"default": {
"ENGINE": "tortoise.backends.mysql",
"CREDENTIALS": {
"HOST": "localhost",
"PORT": 3306,
"USER": "root",
"PASSWORD": "secret",
"DATABASE": "mydb",
},
}
},
"DRIVER": "unfazed.db.tortoise.Driver", # optional, this is the default
"USE_TZ": True, # optional
"TIMEZONE": "Asia/Singapore", # optional
},
}
Top-level fields:
| Field | Type | Default | Description |
|---|---|---|---|
CONNECTIONS |
Dict[str, Connection] |
required | Named database connections. |
DRIVER |
str |
"unfazed.db.tortoise.Driver" |
Dotted path to the driver class. |
APPS |
Dict[str, AppModels] |
None |
Explicit model grouping. Auto-built from installed apps if omitted. |
ROUTERS |
List[str] |
None |
Database router classes for multi-database setups. |
USE_TZ |
bool |
None |
Enable timezone-aware datetimes. |
TIMEZONE |
str |
None |
Default timezone string (e.g. "UTC", "Asia/Singapore"). |
Connection Engines¶
Each connection requires an ENGINE and CREDENTIALS:
| Engine | Database | Async driver |
|---|---|---|
tortoise.backends.mysql |
MySQL / MariaDB | asyncmy |
tortoise.backends.sqlite |
SQLite | aiosqlite |
MySQL Credentials¶
"CREDENTIALS": {
"HOST": "localhost",
"PORT": 3306,
"USER": "root",
"PASSWORD": "secret",
"DATABASE": "mydb",
# optional
"MIN_SIZE": 1,
"MAX_SIZE": 10,
"SSL": False,
"CONNECT_TIMEOUT": 10,
"ECHO": False,
"CHARSET": "utf8mb4",
}
| Field | Type | Required | Description |
|---|---|---|---|
HOST |
str |
yes | Database host. |
PORT |
int |
yes | Database port. |
USER |
str |
yes | Username. |
PASSWORD |
str |
yes | Password. |
DATABASE |
str |
yes | Database name. |
MIN_SIZE |
int |
no | Minimum connection pool size. |
MAX_SIZE |
int |
no | Maximum connection pool size. |
SSL |
bool |
no | Enable SSL. |
CONNECT_TIMEOUT |
int |
no | Connection timeout in seconds. |
ECHO |
bool |
no | Echo SQL statements. |
CHARSET |
str |
no | Character set (e.g. "utf8mb4"). |
SQLite Credentials¶
"CREDENTIALS": {
"FILE_PATH": "./db.sqlite3",
# optional
"JOURNAL_MODE": "wal",
"JOURNAL_SIZE_LIMIT": 16384,
"FOREIGN_KEYS": "ON",
}
| Field | Type | Required | Description |
|---|---|---|---|
FILE_PATH |
str |
yes | Path to the SQLite file. Use ":memory:" for in-memory databases. |
JOURNAL_MODE |
str |
no | SQLite journal mode. |
JOURNAL_SIZE_LIMIT |
int |
no | Journal size limit in bytes. |
FOREIGN_KEYS |
str |
no | Enable foreign key enforcement ("ON" / "OFF"). |
Automatic Model Discovery¶
When DATABASE.APPS is not provided (the default), Unfazed automatically scans all installed apps at startup. Any app whose package contains a models.py module is registered:
UNFAZED_SETTINGS = {
...
"INSTALLED_APPS": [
"apps.account", # has models.py → models collected
"apps.blog", # has models.py → models collected
"apps.utils", # no models.py → skipped
],
}
The auto-generated configuration is equivalent to:
DATABASE["APPS"] = {
"models": {
"MODELS": [
"aerich.models",
"apps.account.models",
"apps.blog.models",
]
}
}
aerich.models is always included to support Aerich's internal migration tracking table.
If you need fine-grained control — for example, routing different apps to different databases — provide APPS explicitly. See Multiple Databases below.
Migration Commands¶
When a DATABASE is configured, Unfazed automatically registers Aerich-based migration commands. All commands accept --location / -l (default ./migrations) to specify the migration directory.
| Command | Description | Extra flags |
|---|---|---|
init-db |
Create the migration directory and generate the initial schema. | --safe / -s (default True) |
migrate |
Generate a new migration file from model changes. | --name / -n (default "update"), --app / -a (default "models") |
upgrade |
Apply pending migrations. | --transaction / -t (default True) |
downgrade |
Revert to a specific migration version. | --version / -v (default -1), --delete / -d (default True) |
history |
List all applied migrations. | — |
heads |
Show available migration heads. | — |
inspectdb |
Reverse-engineer model definitions from existing database tables. | --tables / -t (specific tables to inspect) |
Typical workflow¶
# 1. Initialise the migration directory (once per project)
unfazed-cli init-db
# 2. After changing models, generate a migration
unfazed-cli migrate --name add_published_field
# 3. Apply the migration
unfazed-cli upgrade
# 4. Check migration history
unfazed-cli history
# 5. Rollback the last migration if needed
unfazed-cli downgrade --version -1
Inspecting an existing database¶
To generate model code from tables that already exist:
# Inspect all tables
unfazed-cli inspectdb
# Inspect specific tables
unfazed-cli inspectdb --tables user --tables order
Multiple Databases¶
To route different models to different database connections, provide APPS explicitly with DEFAULT_CONNECTION pointing to the appropriate connection alias.
1. Configure connections and app groups¶
# entry/settings/__init__.py
UNFAZED_SETTINGS = {
...
"INSTALLED_APPS": [
"apps.account",
"apps.analytics",
],
"DATABASE": {
"CONNECTIONS": {
"default": {
"ENGINE": "tortoise.backends.mysql",
"CREDENTIALS": {
"HOST": "primary-db",
"PORT": 3306,
"USER": "app",
"PASSWORD": "secret",
"DATABASE": "main",
},
},
"analytics": {
"ENGINE": "tortoise.backends.mysql",
"CREDENTIALS": {
"HOST": "analytics-db",
"PORT": 3306,
"USER": "app",
"PASSWORD": "secret",
"DATABASE": "analytics",
},
},
},
"APPS": {
"models": {
"MODELS": ["aerich.models", "apps.account.models"],
"DEFAULT_CONNECTION": "default",
},
"analytics_models": {
"MODELS": ["apps.analytics.models"],
"DEFAULT_CONNECTION": "analytics",
},
},
},
}
2. Set app in each model's Meta¶
Each model must declare which app group it belongs to via app = "<group_key>" in class Meta. The value must match the key you defined in APPS:
# apps/account/models.py
from tortoise import Model, fields
class User(Model):
name = fields.CharField(max_length=255)
email = fields.CharField(max_length=255)
class Meta:
table = "user"
app = "models" # matches the "models" key in APPS → uses "default" connection
# apps/analytics/models.py
from tortoise import Model, fields
class EventLog(Model):
event = fields.CharField(max_length=255)
created_at = fields.DatetimeField(auto_now_add=True)
class Meta:
table = "event_log"
app = "analytics_models" # matches the "analytics_models" key in APPS → uses "analytics" connection
With this setup, User queries go to the default connection (primary-db) and EventLog queries go to the analytics connection (analytics-db).
When APPS is provided, automatic model discovery is skipped. You must list all model modules (including aerich.models) yourself.
API Reference¶
Database¶
class Database(BaseModel):
connections: Dict[str, Connection] = Field(..., alias="CONNECTIONS")
driver: str = Field(default="unfazed.db.tortoise.Driver", alias="DRIVER")
apps: Dict[str, AppModels] | None = Field(default=None, alias="APPS")
routers: List[str] | None = Field(default=None, alias="ROUTERS")
use_tz: bool | None = Field(default=None, alias="USE_TZ")
timezone: str | None = Field(default=None, alias="TIMEZONE")
Top-level database configuration model.
Connection¶
class Connection(BaseModel):
engine: str = Field(..., alias="ENGINE")
credentials: SqliteCredential | MysqlCredential = Field(..., alias="CREDENTIALS")
A single named database connection.
AppModels¶
class AppModels(BaseModel):
models: List[str] = Field(..., alias="MODELS")
default_connection: str = Field(default="default", alias="DEFAULT_CONNECTION")
Groups model modules and maps them to a connection alias.
Driver¶
Default Tortoise ORM driver (unfazed.db.tortoise.Driver).
async setup() -> None: Build apps config (if not provided), callTortoise.init(), and load Aerich commands.async migrate() -> None: CallTortoise.generate_schemas()to create tables from models.build_apps() -> Dict[str, AppModels]: Auto-discover models from installed apps and return the Tortoise apps dict.list_aerich_command() -> List[Command]: ReturnCommandobjects for all Aerich CLI commands.
ModelCenter¶
Internal registry that manages the database driver lifecycle.
async setup() -> None: Import the driver class, instantiate it, and calldriver.setup(). No-op ifDATABASEis not configured.async migrate() -> None: Calldriver.migrate(). RaisesValueErrorif the driver has not been set up.
DataBaseDriver (Protocol)¶
class DataBaseDriver(Protocol):
async def setup(self) -> None: ...
async def migrate(self) -> None: ...
Protocol that custom database drivers must satisfy.