Doris-Python:让 SQLAlchemy 自由切换 Doris 同步/异步驱动
背景
Apache Doris 的 Python 生态里,最知名的连接库是 pydoris 官方 SDK。它功能完善、协议实现扎实,是 Doris Python 接入的"事实标准"。
但 pydoris 在 SQLAlchemy 场景下有一个很现实的限制:只支持了一种驱动。也就是说,如果你已经在项目里统一用 SQLAlchemy 来管理连接池、ORM、迁移等能力,又想接入 Doris,那么无论你之前习惯用 mysqldb、pymysql、aiomysql 还是 asyncmy,pydoris 的 SQLAlchemy 方言都会把你"绑定"到一种实现上。
现实中一个常见的搭配是:
- 同步链路 :
SQLAlchemy + PyMySQL,生态最成熟、文档最多; - 异步链路 :
SQLAlchemy 2.0 async + aiomysql或asyncmy,需要在 Web 框架(FastAPI / Starlette / aiohttp)里跑高并发查询。
如果你想用一份统一的 SQLAlchemy 代码同时跑同步和异步,又或者想从 PyMySQL 平滑迁移到异步驱动,pydoris 的"单一驱动"模式就会成为绊脚石。
这正是 doris-python 这个仓库要解决的问题:在 pydoris 基础上,把 SQLAlchemy 体系下 Doris 的同步/异步驱动支持扩展到主流的几个实现,使用者按需选择。 ------ 也就是说,doris-python 并不是要把 pydoris 的能力重写一遍,而是只把"驱动选择权"这件事补齐。
doris-python 是什么
doris-python 是一个 SQLAlchemy Dialect 扩展包,它通过 entry_points 注册多个 Doris 方言,覆盖目前 Python 生态里最常见的三种驱动:
| 同步/异步 | 数据库驱动 | 对应 dialect 名 | 典型场景 |
|---|---|---|---|
| 同步 | mysqldb (mysqlclient) |
doris+mysqldb |
传统服务、ETL 脚本、Django、Flask、命令行工具,C 扩展性能最好 |
| 同步 | pymysql |
doris+pymysql |
不想编译原生依赖、追求纯 Python 部署的传统服务 |
| 异步 | aiomysql |
doris+aiomysql |
FastAPI / Starlette,基于 PyMySQL 的纯 Python 异步驱动 |
| 异步 | asyncmy |
doris+asyncmy |
高并发异步场景,cython 实现的异步驱动,性能更好 |
安装好之后,你只需要在 SQLAlchemy 的 URL 里替换驱动前缀即可:
python
# 同步:mysqldb
engine = create_engine("doris+mysqldb://user:pass@host:9030/mydb")
# 同步:pymysql
engine = create_engine("doris+pymysql://user:pass@host:9030/mydb")
# 异步:aiomysql
engine = create_async_engine("doris+aiomysql://user:pass@host:9030/mydb")
# 异步:asyncmy
engine = create_async_engine("doris+asyncmy://user:pass@host:9030/mydb")
不需要为每种驱动维护一份方言实现,也不需要修改业务代码------切换驱动 = 改 URL 前缀。
核心设计
仓库代码量不大,但结构清晰,全部位于 src/doris_python/sqlalchemy/ 下:
bash
doris_python/
└── sqlalchemy/
├── __init__.py # 暴露各 dialect
├── dialect.py # DorisDialect 基类:Doris/MySQL 方言适配、类型系统、Schema/Compiler
├── datatype.py # Doris 类型映射:DECIMAL/HLL/BITMAP/JSON/ARRAY/MAP 等
├── mysqldb.py # 同步:基于 mysqlclient(mysqldb) 的 Dialect
├── pymysql.py # 同步:基于 pymysql 的 Dialect
├── aiomysql.py # 异步:基于 aiomysql 的 Dialect
└── asyncmy.py # 异步:基于 asyncmy 的 Dialect
几个关键点:
1. 共享一个 DorisDialect 基类
dialect.py 是核心。它继承自 sqlalchemy.dialects.mysql.base.MySQLDialect,复用 SQLAlchemy MySQL 方言的语法/类型能力,再针对 Doris 做"减法"和"特化":
- 适配 MySQL 协议但避免 MySQL 专属语法 :例如对
SHOW VARIABLES、某些信息查询路径做替换; - 数据类型补齐 :Doris 独有的
HLL、BITMAP、QUANTILE_STATE、ARRAY<T>、MAP<K,V>、JSONB等都在datatype.py中显式声明; - Schema/建表语句渲染 :Doris 的
UNIQUE KEY / DUPLICATE KEY / AGGREGATE KEY模型、DISTRIBUTED BY ... BUCKETS ...、PROPERTIES 等都有对应的编译逻辑。
mysqldb / pymysql / aiomysql / asyncmy 四个 driver 文件都是"薄包装",只负责切换执行层,所有 Doris 特有的行为都在基类里共享 。这意味着:新增一种驱动 = 新建一个几十行的 dialect.py 文件。
2. 驱动切换的代价 ≈ 0
因为 dialect 基类共享,三个 driver 之间行为是一致的。举个例子,你在用 pymysql 时这样写:
python
from sqlalchemy import create_engine, text
engine = create_engine("doris+pymysql://user:pass@host:9030/mydb")
with engine.connect() as conn:
rows = conn.execute(text("SELECT version()")).all()
改成异步只是 URL 变化,业务代码变成 async:
python
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
engine = create_async_engine("doris+aiomysql://user:pass@host:9030/mydb")
async with engine.connect() as conn:
rows = (await conn.execute(text("SELECT version()"))).all()
⚠️ 注意:因为
doris+aiomysql/doris+asyncmy是 async engine,所有 DBAPI 调用都要await,并使用AsyncConnection/AsyncSession。
3. Doris 数据类型的完整映射
Doris 相对 MySQL 有不少独有的类型,datatype.py 给出了显式映射,避免落到 SQLAlchemy 默认的 NULL 兜底。重点支持:
- 数值:
TINYINT / SMALLINT / INT / BIGINT / LARGEINT / FLOAT / DOUBLE / DECIMAL - 字符串:
CHAR / VARCHAR / STRING / TEXT - 日期:
DATE / DATETIME - Doris 特有:
HLL(近似去重)、BITMAP(位图)、QUANTILE_STATE(分位数近似)、JSON / JSONB、ARRAY<T>、MAP<K,V>、STRUCT<...> - 布尔:
BOOLEAN(在 Doris 中实际是TINYINT,基类做了归一)
建表时的 DUPLICATE/UNIQUE/AGGREGATE KEY 模型和 DISTRIBUTED BY ... BUCKETS N 也是基类编译出来的,不需要业务侧手动拼字符串。
安装与依赖
doris-python 本身不强制安装具体驱动,按需安装即可:
bash
# 基础
pip install doris-python
# 同步:mysqldb(mysqlclient,C 扩展)
pip install "doris-python[mysqldb]"
# 同步:pymysql(纯 Python)
pip install "doris-python[pymysql]"
# 异步:aiomysql
pip install "doris-python[aiomysql]"
# 异步:asyncmy
pip install "doris-python[asyncmy]"
# 全部驱动
pip install "doris-python[all]"
可选依赖通过 pyproject.toml 的 extras 管理,按需拉取,避免污染用户的运行时环境。
实际用法
场景一:mysqldb(同步,性能优先)
mysqlclient(即 mysqldb)是对 libmysqlclient 的 C 扩展封装,是 SQLAlchemy 同步链路里性能最好的 MySQL 驱动 。如果你已经有一份用 mysqlclient 跑 MySQL 的代码,迁移到 Doris 几乎是零改动:
python
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, Session
Base = declarative_base()
class Order(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True)
sku = Column(String(64))
engine = create_engine("doris+mysqldb://user:pass@127.0.0.1:9030/demo")
# 建表(Doris 风格的 CREATE TABLE 由 dialect 渲染)
Base.metadata.create_all(engine)
with Session(engine) as s:
s.add(Order(id=1, sku="A001"))
s.commit()
场景二:PyMySQL(同步,部署简单)
PyMySQL 是纯 Python 实现,免编译、跨平台、部署最简单。代码与场景一完全一致,只是把 driver 换掉:
python
engine = create_engine("doris+pymysql://user:pass@127.0.0.1:9030/demo")
# 后续 Session、ORM、查询等用法完全相同
场景三:aiomysql(异步,生态成熟)
aiomysql 是基于 PyMySQL 的纯 Python 异步实现,迁移成本最低:
python
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy import text
engine = create_async_engine("doris+aiomysql://user:pass@127.0.0.1:9030/demo")
async def main():
async with AsyncSession(engine) as s:
await s.execute(text("SELECT 1"))
rows = (await s.execute(text("SELECT version()"))).all()
print(rows)
import asyncio; asyncio.run(main())
场景四:asyncmy(异步,性能优先)
asyncmy 是 cython 实现的异步 MySQL 客户端,相比 aiomysql 在高并发下性能更好,适合对吞吐有要求的 FastAPI 服务:
python
engine = create_async_engine("doris+asyncmy://user:pass@127.0.0.1:9030/demo")
# 后续用法与 aiomysql 完全一致
同样地,因为 dialect 基类共享,业务代码在三个 driver 间切换是透明的------只是连接 URL 前缀不同。
选型建议
简单说怎么选:
- 同步 + 极致性能(ETL、长任务) :选
doris+mysqldb,C 扩展吞吐最高; - 同步 + 不想编译原生依赖 :选
doris+pymysql,纯 Python 部署最简单; - 异步 + 追求纯 Python 部署 :选
doris+aiomysql; - 异步 + 高并发吞吐 :选
doris+asyncmy,cython 实现性能更好。
业务侧推荐保持一致的 URL 写法:driver 部分只放 +xxx,业务代码不感知具体驱动,方便后续在测试或压测场景随时切换。
与 pydoris 的关系
需要强调一点:doris-python 不是一个平替,而是 pydoris SQLAlchemy 集成在多驱动场景下的扩展 。如果你的项目只用了 pydoris 原生客户端(不走 SQLAlchemy),那 pydoris 完全够用;但只要你的项目是"SQLAlchemy 一统天下",又希望 Doris 也能接进来、又希望在同步/异步驱动之间自由切换,那么 doris-python 就是为这个场景准备的。
简而言之:
| 需求 | 选型 |
|---|---|
| 纯 pydoris 客户端调用 | pydoris |
| SQLAlchemy + 只用一种驱动 | pydoris 或 doris-python 都可 |
| SQLAlchemy + 同步/异步驱动可切换 | doris-python |
项目地址
仓库已开源,欢迎提 Issue / PR:
bash
git clone https://github.com/<your-org>/doris-python.git
cd doris-python && pip install -e ".[all]"
如果你正在被"SQLAlchemy 接入 Doris 该用哪个驱动"困扰,不妨从 doris+pymysql 开始,三行代码跑通后,再无负担地切到 doris+asyncmy 把同步链路异步化------这就是这个仓库存在的意义。