多数据库路由

在 Tortoise.init() 的 config 中,connections 字典支持定义任意数量的命名连接。 每个连接支持两种写法:URL 字符串或完整 credentials 字典。 URL 字符串写法: 完整 credentials 写法(可配置连接池): credentials 通用参数: 每个 app 可以绑定到不同的数据库连接,通过 default_connection 指定: 同一 app 下的所有模型默认使用该 app 的 default_connection,除非模型自身通过 Meta 覆盖。 在模型定义中通过 Meta.using

分享

官方文档:https://tortoise.github.io/databases.html
适用版本:tortoise-orm 0.17+(2026-05-08 核实)
最后更新:2026-04-11


一、配置多个数据库连接

Tortoise.init()config 中,connections 字典支持定义任意数量的命名连接。

1.1 connections 配置参数

每个连接支持两种写法:URL 字符串或完整 credentials 字典。

URL 字符串写法:

TORTOISE_ORM = {
    "connections": {
        "default": "mysql://user:pass@localhost:3306/main_db",
        "replica": "postgres://user:pass@replica-host:5432/read_db",
    },
    ...
}

完整 credentials 写法(可配置连接池):

TORTOISE_ORM = {
    "connections": {
        "default": {
            "engine": "tortoise.backends.mysql",
            "credentials": {
                "host": "localhost",
                "port": 3306,
                "user": "myuser",
                "password": "mypass",
                "database": "main_db",
                "minsize": 1,
                "maxsize": 20,
                "connect_timeout": 5,
            },
        },
        "replica": {
            "engine": "tortoise.backends.asyncpg",
            "credentials": {
                "host": "replica-host",
                "port": 5432,
                "user": "readonly_user",
                "password": "readpass",
                "database": "read_db",
                "minsize": 2,
                "maxsize": 50,
            },
        },
    },
    ...
}

credentials 通用参数:

参数 类型 默认值 说明
host str "127.0.0.1" 数据库主机地址
port int 因引擎而异 数据库端口
user str 无,必填 数据库用户名
password str 无,必填 数据库密码
database str 无,必填 数据库名称
minsize int 1 连接池最小连接数
maxsize int 10 连接池最大连接数
connect_timeout float None 连接超时秒数
ssl bool | SSLContext None 是否启用 SSL

二、apps 中指定 default_connection

每个 app 可以绑定到不同的数据库连接,通过 default_connection 指定:

TORTOISE_ORM = {
    "connections": {
        "primary": "mysql://user:pass@primary-host:3306/main_db",
        "analytics": "postgres://user:pass@analytics-host:5432/analytics_db",
    },
    "apps": {
        "models": {
            "models": ["myapp.models", "aerich.models"],
            "default_connection": "primary",   # 此 app 下的模型默认使用 primary
        },
        "analytics": {
            "models": ["myapp.analytics.models"],
            "default_connection": "analytics", # analytics app 使用独立数据库
        },
    },
}

同一 app 下的所有模型默认使用该 app 的 default_connection,除非模型自身通过 Meta 覆盖。


三、Model.Meta.using 绑定模型到指定数据库

在模型定义中通过 Meta.using 硬编码模型与数据库连接的绑定关系:

from tortoise.models import Model
from tortoise import fields


class AnalyticsEvent(Model):
    id = fields.IntField(pk=True)
    event_type = fields.CharField(max_length=100)
    user_id = fields.IntField()
    occurred_at = fields.DatetimeField(auto_now_add=True)

    class Meta:
        table = "analytics_event"
        using = "analytics"   # 所有对此模型的操作默认使用 analytics 连接


class User(Model):
    id = fields.IntField(pk=True)
    username = fields.CharField(max_length=50)

    class Meta:
        table = "user"
        # 不指定 using,使用所在 app 的 default_connection

四、查询时用 using_db 切换数据库

即使模型已绑定默认连接,也可以在单次查询时通过 using_db 临时切换:

from tortoise import Tortoise


# 获取指定连接
replica_conn = Tortoise.get_connection("replica")

# 查询时指定连接
users = await User.all().using_db(replica_conn)
user = await User.filter(id=1).using_db(replica_conn).first()

# 单次写操作切换到主库
primary_conn = Tortoise.get_connection("primary")
await User.create(username="alice", using_db=primary_conn)

using_db 参数说明:

参数 类型 默认值 说明
using_db BaseDBAsyncClient None 指定使用的数据库连接对象;None 则使用模型默认连接

五、读写分离配置模式

5.1 配置结构

TORTOISE_ORM = {
    "connections": {
        "master": {
            "engine": "tortoise.backends.mysql",
            "credentials": {
                "host": "primary.db.internal",
                "port": 3306,
                "user": "app_user",
                "password": "strongpass",
                "database": "mydb",
                "maxsize": 20,
            },
        },
        "slave": {
            "engine": "tortoise.backends.mysql",
            "credentials": {
                "host": "replica.db.internal",
                "port": 3306,
                "user": "readonly_user",
                "password": "readpass",
                "database": "mydb",
                "maxsize": 50,   # 读库可以配更大连接池
            },
        },
    },
    "apps": {
        "models": {
            "models": ["myapp.models", "aerich.models"],
            "default_connection": "master",  # 默认写主库
        }
    },
}

5.2 封装读写路由工具

# db.py
from tortoise import Tortoise


def get_master():
    """获取主库连接(写操作)"""
    return Tortoise.get_connection("master")


def get_slave():
    """获取从库连接(读操作)"""
    return Tortoise.get_connection("slave")

5.3 在 Service 层应用读写分离

# services/user_service.py
from myapp.db import get_master, get_slave
from myapp.models import User


async def get_user(user_id: int) -> User | None:
    """读操作走从库"""
    return await User.get_or_none(id=user_id).using_db(get_slave())


async def list_users() -> list[User]:
    """读操作走从库"""
    return await User.all().using_db(get_slave())


async def create_user(username: str, email: str) -> User:
    """写操作走主库"""
    return await User.create(username=username, email=email, using_db=get_master())


async def update_user(user_id: int, **kwargs) -> int:
    """写操作走主库"""
    return await User.filter(id=user_id).using_db(get_master()).update(**kwargs)

六、完整配置示例(MySQL 主库 + PostgreSQL 只读副本)

这种架构适合数据写入用 MySQL,分析查询用 PostgreSQL(通过 ETL 同步数据)的场景。

# config.py
import os

TORTOISE_ORM = {
    "connections": {
        "default": {
            "engine": "tortoise.backends.mysql",
            "credentials": {
                "host": os.getenv("MYSQL_HOST", "localhost"),
                "port": int(os.getenv("MYSQL_PORT", "3306")),
                "user": os.getenv("MYSQL_USER", "app_user"),
                "password": os.getenv("MYSQL_PASSWORD", ""),
                "database": os.getenv("MYSQL_DATABASE", "mydb"),
                "minsize": 2,
                "maxsize": 20,
                "connect_timeout": 10,
                "ssl": os.getenv("MYSQL_SSL", "false").lower() == "true",
            },
        },
        "pg_readonly": {
            "engine": "tortoise.backends.asyncpg",
            "credentials": {
                "host": os.getenv("PG_HOST", "localhost"),
                "port": int(os.getenv("PG_PORT", "5432")),
                "user": os.getenv("PG_USER", "readonly_user"),
                "password": os.getenv("PG_PASSWORD", ""),
                "database": os.getenv("PG_DATABASE", "analytics_db"),
                "minsize": 1,
                "maxsize": 30,
            },
        },
    },
    "apps": {
        "models": {
            "models": ["myapp.models", "aerich.models"],
            "default_connection": "default",   # 主要业务模型走 MySQL
        },
        "reports": {
            "models": ["myapp.reports.models"],
            "default_connection": "pg_readonly",  # 报表模型走 PostgreSQL
        },
    },
    "use_tz": True,
    "timezone": "Asia/Shanghai",
}
# models.py
from tortoise.models import Model
from tortoise import fields


class User(Model):
    """主业务模型,存储于 MySQL"""
    id = fields.IntField(pk=True)
    username = fields.CharField(max_length=50, unique=True)
    email = fields.CharField(max_length=255)
    created_at = fields.DatetimeField(auto_now_add=True)

    class Meta:
        table = "user"
        # default_connection 由 app 的 default_connection 决定(即 "default"/MySQL)


# reports/models.py
class UserReport(Model):
    """报表模型,读取 PostgreSQL 中的只读视图"""
    user_id = fields.IntField()
    total_orders = fields.IntField()
    total_spend = fields.DecimalField(max_digits=12, decimal_places=2)
    report_date = fields.DateField()

    class Meta:
        table = "v_user_report"   # PostgreSQL 视图
        using = "pg_readonly"     # 明确绑定到 PostgreSQL 连接
# main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from tortoise import Tortoise
from myapp.config import TORTOISE_ORM


@asynccontextmanager
async def lifespan(app: FastAPI):
    await Tortoise.init(config=TORTOISE_ORM)
    yield
    await Tortoise.close_connections()


app = FastAPI(lifespan=lifespan)

七、事务跨数据库注意事项

7.1 事务只在单个连接内有效

Tortoise ORM 的事务绑定到具体连接,不支持跨数据库的分布式事务。

from tortoise.transactions import in_transaction


# 正确:事务限定在单个连接内
async with in_transaction("default") as conn:
    user = await User.create(username="alice", using_db=conn)
    await Profile.create(user_id=user.id, using_db=conn)
    # User 和 Profile 在同一个 "default" 数据库,事务有效


# 错误:跨库操作不在同一事务中
async with in_transaction("default") as conn:
    user = await User.create(username="alice", using_db=conn)
    # AnalyticsEvent 在 "analytics" 数据库,此操作不在上面的事务内
    await AnalyticsEvent.create(user_id=user.id)  # 独立执行,不受事务保护

跨数据库操作需要在应用层自行处理一致性,例如使用补偿事务或消息队列。

7.2 in_transaction 参数

from tortoise.transactions import in_transaction

async with in_transaction(connection_name="default") as conn:
    ...
参数 类型 默认值 说明
connection_name str "default" 要开启事务的连接名称

八、踩坑与注意事项

8.1 事务不能跨数据库

如上文所述,in_transaction 只对指定连接生效。如果在 in_transaction("default") 上下文中对绑定到其他连接的模型执行写操作,该操作不在事务保护范围内,发生错误时不会自动回滚。

解决方案:将需要保持原子性的数据放在同一个数据库中。

8.2 外键约束只在同库有效

不同数据库之间的表不能建立数据库层面的外键约束,即使在 Tortoise 模型层定义了 ForeignKeyField,底层数据库也无法跨库执行约束检查。

class Order(Model):
    # 错误:user 在 MySQL,Order 在 PostgreSQL,数据库层的外键约束不生效
    user: fields.ForeignKeyRelation["User"] = fields.ForeignKeyField(
        "models.User", related_name="orders"
    )

    class Meta:
        using = "pg_readonly"

跨数据库的关联完整性需要在应用层代码中手动维护(查询前校验、删除前清理)。

8.3 aerich 迁移仅作用于绑定的连接

Aerich 迁移针对的是 app 的 default_connection。若有多个 app 对应不同数据库,需要分别运行迁移:

aerich upgrade --app models      # 迁移 MySQL 主库
aerich upgrade --app analytics   # 迁移 PostgreSQL 分析库

详见 Aerich迁移指南 中的多应用配置章节。

8.4 连接名称拼写错误导致静默失败

Tortoise.get_connection("typo_name") 在连接名不存在时会抛出 ConfigurationError,但如果连接名在 Meta.using 中写错,会在第一次查询时才报错,而非启动时。

建议将连接名定义为常量:

# constants.py
DB_PRIMARY = "primary"
DB_REPLICA = "replica"
DB_ANALYTICS = "analytics"
class AnalyticsEvent(Model):
    class Meta:
        using = DB_ANALYTICS  # 引用常量,避免拼写错误

最佳实践

用常量代替魔法字符串命名连接DB_DEFAULT = "default" 定义常量,在 Meta.usingusing_db() 中引用常量,拼写错误在开发期即可发现(IDE 提示)。

跨库事务必须用同一数据库连接:Tortoise ORM 的事务绑定单个数据库连接,跨不同数据库的操作无法放在同一原子事务中,需在应用层设计补偿机制(Saga 模式)。

只读副本用 using_db("readonly") 路由:将耗时报表查询路由到只读副本,减轻主库压力;读写分离时注意主从延迟,避免写后立即从副本读取。

Tortoise.get_connection() 获取连接做原生 SQL:需要多数据库的复杂 SQL 时,用 conn = Tortoise.get_connection("analytics")await conn.execute_query(sql) 精确指定连接。

aerich 迁移按 app 独立管理:多数据库场景下,每个 app 的迁移文件目录独立,aerich upgrade --app <name> 分别执行,避免迁移历史混淆。


常见陷阱

陷阱:未配置 Meta.using 的模型路由到意外的数据库

现象: 新模型未设置 Meta.using,查询时报找不到表,或写入到了错误的数据库。
原因: 没有 Meta.using 的模型默认路由到 "default" 连接,若该模型的表在其他数据库则找不到。
解决: 严格为非 default 数据库的模型设置 Meta.using,并在 Code Review 中检查新 Model 是否遗漏。

陷阱:register_tortoiseapps 配置遗漏模型

现象: 多数据库配置时某个 app 的模型不生效,查询报表不存在。
原因: TORTOISE_ORM["apps"]models 列表未包含该模型的模块路径。
解决: 检查 apps 配置,确保所有模型模块路径都在正确 app 的 models 列表中;generate_schemas=True 调试时可检查哪些表被创建。

陷阱:在事务中混用两个不同数据库的操作

现象: @atomic() 装饰的函数中同时操作 defaultanalytics 数据库,其中一个出错后另一个没有回滚。
原因: @atomic() 的事务只能绑定单一连接,跨连接操作无法原子化。
解决: 设计时避免跨库原子操作;如果必须保证一致性,用消息队列 + 幂等操作 + 补偿事务实现最终一致性。


参见

初始化与配置
Aerich迁移指南
查询操作完全指南

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog