Tortoise-ORM Quick Start: Write asynchronous database operations like writing django

📂 Phase: Phase 3 - Data Persistence (Database) 🔗 Related chapters: SQLAlchemy 2.0 实战 · Redis 集成 · FastAPI 路由


1. Why choose Tortoise-ORM? Say goodbye to SQLAlchemy

If SQLAlchemy is compared to a fully functional Swiss Army knife, then Tortoise-ORM is a folding scissor tailored for Python asynchronous development - lightweight, intuitive, and ready to use out of the box. For small and medium-sized projects and rapid prototyping, it allows you to seamlessly transition from Django ORM to the asynchronous world with almost zero learning cost.

1.1 One-minute comparison with SQLAlchemy 2.0

Core pain pointsTortoise-ORM solutionSQLAlchemy 2.0 solution
Learning costFully reuses the API style of django ORM, no additional mental burdenVersion 2.0 provides both Core and ORM paradigms, with many levels of concepts, and it often takes 1–2 weeks to get started
Asynchronous experienceNative from the beginning of designasync/await, there is no synchronous to asynchronous switching split2.0 truly supports native asynchronous, but the bottom layer is still based on the synchronization engine, and the asynchronous session life cycle must be strictly managed
Configuration complexityA Python dictionary can define connections, models, and time zonesNeed to manually build an asynchronous engine, session factory, and handle context transfer

To put it simply: **If you like the expression of django ORM and need native asynchronous support, Tortoise-ORM is the best answer. **

1.2 Set up the development environment with one line of commands

Choose dependencies according to the database you use, do not install them all:

# 开发/测试首选:SQLite(无需额外服务)
pip install tortoise-orm[sqlite]

# 生产环境推荐:PostgreSQL(性能最优)
pip install tortoise-orm[asyncpg] asyncpg

2. Initialization: two files to get the project running

The initialization of Tortoise-ORM is very refreshing: configuration and startup logic are separated, and the entire process only requires two files.

2.1 Configuration filetortoise_config.py

Centrally manage connection information, model paths, time zones, etc., and then just import a dictionary:

from tortoise import Tortoise

TORTOISE_ORM = {
    "connections": {
        "default": {
            "engine": "tortoise.backends.sqlite",       # 开发用 SQLite
            # "engine": "tortoise.backends.asyncpg",   # 生产用 PostgreSQL
            "credentials": {
                "file_path": "./dev.db",                # SQLite 文件路径
                # "host": "localhost",
                # "port": 5432,
                # "user": "dev_user",
                # "password": "dev_pwd",
                # "database": "tortoise_demo",
            }
        }
    },
    "apps": {
        # 自定义 app 名称,后续模型引用时使用 `app名.模型类名`
        "demo": {
            "models": ["__main__", "demo_models"],      # 模型所在的模块路径
            "default_connection": "default",
        }
    },
    "use_tz": False,          # 国内项目一般无需 UTC 转换
    "timezone": "Asia/Shanghai",
}

2.2 One-click mounting in FastAPI

FastAPI community providesregister_tortoiseTool to automatically handle the startup, shutdown and table creation of database connections:

from fastapi import FastAPI
from tortoise.contrib.fastapi import register_tortoise
from tortoise_config import TORTOISE_ORM

app = FastAPI(title="Tortoise + FastAPI CRUD 演示")

register_tortoise(
    app,
    config=TORTOISE_ORM,
    generate_schemas=True,             # 开发时自动建表(生产环境建议使用 Aerich 迁移工具)
    add_exception_handlers=True,       # 自动捕获模型验证、不存在等异常
)

Tip: The migration tool Aerich should be used in production environments to manage table structure changes instead of relying ongenerate_schemas=True


3. Model definition: familiar django ORM recipe

Newdemo_models.py, defining a set of classic blog models: author, tag, article, covering common fields such as foreign keys, many-to-many, automatic timestamps, etc.

from tortoise import fields
from tortoise.models import Model

class Author(Model):
    id = fields.IntField(pk=True, autoincrement=True)
    name = fields.CharField(max_length=50, description="作者姓名")
    email = fields.CharField(max_length=100, unique=True, description="唯一邮箱")
    # 反向关联提示(非必需,但能提供 IDE 自动补全)
    articles: fields.ReverseRelation["Article"]

    class Meta:
        table = "authors"
        ordering = ["name"]

    def __str__(self):
        return self.name


class Tag(Model):
    id = fields.IntField(pk=True)
    name = fields.CharField(max_length=30, unique=True, description="标签名")
    articles: fields.ReverseRelation["Article"]

    def __str__(self):
        return self.name


class Article(Model):
    id = fields.IntField(pk=True)
    title = fields.CharField(max_length=200, index=True, description="文章标题(加索引加速搜索)")
    content = fields.TextField(description="文章正文")
    views = fields.IntField(default=0, description="阅读量")
    is_published = fields.BooleanField(default=False, description="是否发布")
    created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
    updated_at = fields.DatetimeField(auto_now=True, description="更新时间")

    # 外键:删除作者时级联删除其所有文章
    author = fields.ForeignKeyField(
        "demo.Author",            # 格式:app名.模型类名
        related_name="articles",
        on_delete=fields.CASCADE,
    )
    # 多对多:通过 article_tag 中间表关联标签
    tags = fields.ManyToManyField(
        "demo.Tag",
        related_name="articles",
        through="article_tag",
    )

    class Meta:
        table = "articles"
        ordering = ["-created_at"]   # 最新文章排在最前

After the definition is completed, Tortoise-ORM will automatically generate the corresponding database table based on the model, and foreign keys and many-to-many relationships will also be established, eliminating the need for handwritten SQL at all.


4. CRUD practice: remember one thingawaitThat's enough

All database operations must be performed within an asynchronous function, preceded byawait. The following example uses a pure Tortoise-ORM interface and does not rely on any web framework, making it easy for you to reuse it anywhere.

4.1 Create

# 单条创建
new_author = await Author.create(
    name="张三",
    email="zhangsan@example.com"
)

# 批量创建(比循环单个创建快 10 倍以上)
await Tag.bulk_create([
    Tag(name="Python"),
    Tag(name="FastAPI"),
    Tag(name="Tortoise-ORM"),
])

# 创建文章并关联作者和标签
new_article = await Article.create(
    title="Tortoise-ORM 入门第一弹",
    content="今天我们来学习 Tortoise-ORM ...",
    author=new_author   # 也可以直接传 author_id=1
)

python_tag = await Tag.get(name="Python")
tortoise_tag = await Tag.get(name="Tortoise-ORM")
await new_article.tags.add(python_tag, tortoise_tag)

4.2 Read

Key Principle: If you want to access foreign key or many-to-many related fields, always useprefetch_related()Load data in advance, otherwise it will easily cause N+1 query problems.

# 查询单条:若不存在则抛出 DoesNotExist 异常
author = await Author.get(id=1)

# 安全查询:找不到返回 None
article = await Article.get_or_none(id=999)

# 过滤查询
published_articles = await Article.filter(is_published=True)

# 跨表过滤(使用双下划线语法)
python_articles = await Article.filter(tags__name="Python").prefetch_related("author", "tags")

# 分页与排序
page1 = await Article.all().offset(0).limit(10).order_by("-views")

# 只查询需要的字段(减少数据传输)
titles = await Article.all().only("id", "title")

# 聚合统计:计算每个作者的文章数
from tortoise.functions import Count

author_stats = await Author.annotate(article_count=Count("articles"))
for stat in author_stats:
    print(f"{stat.name} 写了 {stat.article_count} 篇文章")

4.3 Update

# 先查再改,适合需要业务逻辑判断的场景
article = await Article.get(id=1)
article.title = "修改后的标题"
article.is_published = True
await article.save()

# 批量更新(性能更好)
await Article.filter(views__gt=1000).update(is_published=True)

4.4 Delete

# 单条删除
article = await Article.get(id=1)
await article.delete()

# 批量删除
await Article.filter(is_published=False, created_at__lt="2024-01-01").delete()

5. Integrate FastAPI: write a complete REST interface in 5 minutes

Combine the Pydantic model with the previous CRUD logic, and a ready-to-test API is born.

from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, EmailStr
from typing import List
from demo_models import Author, Article, Tag

app = FastAPI(title="Tortoise + FastAPI CRUD 演示")

# ------------------------------ Pydantic 模型 ------------------------------
class AuthorIn(BaseModel):
    name: str
    email: EmailStr

class AuthorOut(BaseModel):
    id: int
    name: str
    email: EmailStr

    class Config:
        from_attributes = True   # 允许直接从 Tortoise 模型转换为 Pydantic

# ------------------------------ 接口实现 ----------------------------------
@app.post("/authors", response_model=AuthorOut, status_code=201)
async def create_author(data: AuthorIn):
    # 尽管模型有 unique 约束,显式检查可以返回更友好的错误信息
    exists = await Author.exists(email=data.email)
    if exists:
        raise HTTPException(status_code=400, detail="该邮箱已被注册")
    return await Author.create(**data.model_dump())

@app.get("/authors", response_model=List[AuthorOut])
async def list_authors(
    skip: int = Query(0, ge=0),
    limit: int = Query(10, ge=1, le=50)
):
    return await Author.all().offset(skip).limit(limit)

Tips: The above is just the simplest example. In actual development, you can add article creation, filtering by tags, reading volume update and other interfaces according to business needs, and combine the CRUD methods learned previously.


6. Summary: Cheat Sheet and Selection Suggestions

6.1 Five-minute quick check card

from tortoise import Tortoise

# 初始化(主程序入口)
await Tortoise.init(config=TORTOISE_ORM)
await Tortoise.generate_schemas()          # 首次建表(生产环境用 Aerich)

# 创建
obj = await Model.create(**kwargs)
await Model.bulk_create([Model(...), Model(...)])

# 查询
obj = await Model.get_or_none(id=1)        # 安全版,找不到返回 None
objs = await Model.filter(...)
    .prefetch_related("关联字段")
    .order_by("-时间字段")
    .offset(skip)
    .limit(limit)

# 更新
obj.field = "新值"
await obj.save()
await Model.filter(...).update(field="新值")

# 删除
await obj.delete()
await Model.filter(...).delete()

6.2 When to choose Tortoise-ORM?

HIGHLY RECOMMENDED

  • You like the way django ORM is written and don’t want to learn a new ORM
  • The project is a small to medium-sized system, rapid prototype or MVP
  • The technology stack is based on asynchronous frameworks such as FastAPI / Starlette

Choose carefully

  • Large enterprise-level systems requiring extremely granular SQL control
  • There is a large amount of historical code based on SQLAlchemy
  • Database migration logic is extremely complex and requires a high degree of customization

🔗Extended resources