| name | tortoise-orm |
| description | [Applies to: **/*.py] Definitive guidelines for using Tortoise ORM effectively, focusing on async patterns, robust model design, efficient querying, and production-ready migrations. |
| source | cursor_mdc |
tortoise-orm Best Practices
Tortoise ORM is an excellent choice for asyncio-native Python applications. Adhere to these guidelines for maintainable, performant, and secure database interactions.
1. Async Lifecycle Management
Always initialize and close connections explicitly. Failing to do so leads to resource leaks and unpredictable behavior.
❌ BAD: Dangling connections
from tortoise import Tortoise
async def main():
await Tortoise.init(db_url='sqlite://:memory:', modules={'models': ['app.models']})
✅ GOOD: Proper async context management
from tortoise import Tortoise, run_async
async def init_db():
await Tortoise.init(
db_url='sqlite://:memory:',
modules={'models': ['app.models']}
)
await Tortoise.generate_schemas()
async def close_db():
await Tortoise.close_connections()
async def run_app():
await init_db()
try:
pass
finally:
await close_db()
run_async(run_app())
2. Production Migrations with Aerich
Tortoise.generate_schemas() is for development convenience only. For production, use Aerich, the official migration tool.
❌ BAD: Using generate_schemas in production
await Tortoise.init(...)
await Tortoise.generate_schemas()
✅ GOOD: Aerich for schema evolution
- Install Aerich:
pip install "aerich[toml]"
- Configure
TORTOISE_ORM to include aerich.models:
TORTOISE_ORM = {
"connections": {"default": "postgres://user:pass@host:port/db"},
"apps": {
"models": {
"models": ["app.models", "aerich.models"],
"default_connection": "default",
},
},
}
- Initialize Aerich:
aerich init -t config.TORTOISE_ORM
- Generate initial migration:
aerich init-db
- Generate subsequent migrations:
aerich migrate
- Apply migrations:
aerich upgrade
3. Robust Model Design
Define models clearly with explicit types, primary keys, and Meta options.
3.1 Base Model & Primary Keys
Every model must inherit from tortoise.models.Model and define a primary key. Use recommended primary key types.
❌ BAD: Implicit primary key, missing type hints
from tortoise.models import Model
from tortoise import fields
class User(Model):
name = fields.TextField()
✅ GOOD: Explicit primary key, type hints
from tortoise.models import Model
from tortoise import fields
from uuid import UUID
class User(Model):
id: UUID = fields.UUIDField(primary_key=True)
name: str = fields.CharField(max_length=255, unique=True)
created_at: fields.DatetimeField = fields.DatetimeField(auto_now_add=True)
updated_at: fields.DatetimeField = fields.DatetimeField(auto_now=True)
class Meta:
table = "users"
3.2 Abstract Models for Reusability
Use abstract models for common fields to avoid repetition.
from tortoise.models import Model
from tortoise import fields
class TimestampMixin(Model):
created_at = fields.DatetimeField(null=True, auto_now_add=True)
updated_at = fields.DatetimeField(null=True, auto_now=True)
class Meta:
abstract = True
class Product(TimestampMixin):
id: int = fields.IntField(primary_key=True)
name: str = fields.CharField(max_length=100)
price: float = fields.DecimalField(max_digits=10, decimal_places=2)
class Meta:
table = "products"
3.3 Model Discovery (__models__)
For explicit model discovery, define __models__ in your model modules.
from tortoise.models import Model
from tortoise import fields
class Category(Model):
id: int = fields.IntField(primary_key=True)
name: str = fields.CharField(max_length=50, unique=True)
class Item(Model):
id: int = fields.IntField(primary_key=True)
name: str = fields.CharField(max_length=100)
category: fields.ForeignKeyRelation["Category"] = fields.ForeignKeyField('models.Category', related_name='items')
__models__ = [
"app.models.Category",
"app.models.Item",
]
4. Efficient Querying (Avoid N+1)
Always eager-load related objects to prevent the N+1 query problem.
❌ BAD: N+1 queries
categories = await Category.all()
for category in categories:
print(f"Category: {category.name}")
items = await category.items.all()
for item in items:
print(f" Item: {item.name}")
✅ GOOD: Eager loading with select_related or fetch_related
items_with_categories = await Item.all().select_related('category')
for item in items_with_categories:
print(f"Item: {item.name}, Category: {item.category.name}")
categories_with_items = await Category.all()
await categories_with_items.fetch_related('items')
for category in categories_with_items:
print(f"Category: {category.name}")
for item in category.items:
print(f" Item: {item.name}")
5. Batch Operations
Use bulk_create and bulk_update for inserting/updating multiple records efficiently.
❌ BAD: Individual saves in a loop
for data in list_of_data:
await MyModel.create(**data)
✅ GOOD: Batch operations
from datetime import datetime
objects_to_create = [MyModel(**data) for data in list_of_data]
await MyModel.bulk_create(objects_to_create)
objects_to_update = await MyModel.filter(status="pending")
for obj in objects_to_update:
obj.status = "processed"
obj.updated_at = datetime.now()
await MyModel.bulk_update(objects_to_update, fields=["status", "updated_at"])
6. Transactions for Atomicity
Ensure data consistency with atomic transactions for related operations.
from tortoise.transactions import in_transaction
class Account(Model):
id: int = fields.IntField(primary_key=True)
balance: float = fields.DecimalField(max_digits=10, decimal_places=2)
async def transfer_funds(from_account_id: int, to_account_id: int, amount: float):
async with in_transaction() as connection:
from_account = await Account.get(id=from_account_id)
to_account = await Account.get(id=to_account_id)
if from_account.balance < amount:
raise ValueError("Insufficient funds")
from_account.balance -= amount
to_account.balance += amount
await from_account.save(update_fields=["balance"], using_db=connection)
await to_account.save(update_fields=["balance"], using_db=connection)
7. Version Pinning
Pin your tortoise-orm version in requirements.txt to a stable minor release (e.g., 0.25.x) to avoid unexpected breaking changes.
❌ BAD: Loose dependency
tortoise-orm
✅ GOOD: Pinned dependency
tortoise-orm==0.25.10 # Pin to a specific patch version
# or
tortoise-orm~=0.25.0 # Pin to a minor version range