一个写过 MyBatis 的人第一次用 SQLAlchemy,往往会冒出两个极端的念头:要么觉得"这不就是把 MyBatis 换个马甲?",要么在 Session 的坑里摔得怀疑人生------"怎么连个 sqlSession 都找不到?"
这篇我们用对照 MyBatis 的方式拆解 Python 生态的数据访问层,搞懂三件事:Python 的 ORM 为什么敢叫"全自动"?它的抽象在什么时候会"漏"?以及------一个连 SQL 都替你生成的全自动 ORM,到底把控制权交给了谁?

一、从 MyBatis 到 SQLAlchemy:两种哲学的对撞
先摆一张对照表,这是全文的骨架:
| 维度 | MyBatis(Java 半自动) | SQLAlchemy / Django ORM(Python 全自动) |
|---|---|---|
| SQL 谁写 | 你写,XML/注解里手写 | 框架生成,你用 Python 对象表达意图 |
| 映射谁管 | 手写 resultMap / 注解 | 声明式模型类自动映射 |
| 灵活性 | 极高(SQL 尽在掌握) | 中高(复杂 SQL 可用原生语句兜底) |
| 学习曲线 | 中间件思维,上手快 | 概念多(Session/事务/声明式) |
| 擅长 | 复杂查询、DBA 主导、需要精细控制 | CRUD、快速开发、跟随模型演进 |
一句话理解差异: MyBatis 是"你在 SQL 世界的代表人 "------SQL 始终由你书写,框架只负责在 Java 对象和结果集之间做搬运;SQLAlchemy 则是"替你写 SQL 的翻译官"------你用 Python 对象描述"要什么",它负责翻译成 SQL 并执行。
MyBatis 把手(SQL)留给你,把"腿"(映射)拿走;SQLAlchemy 把手也拿走了------这就是"半自动"和"全自动"的根本分野。
二、选库先行:SQLite / MySQL / PostgreSQL,别纠结太久
数据访问层第一步不是写代码,是选库。很多人在这上面反复横跳浪费时间。给你一个务实判断:
| 数据库 | 定位 | 何时选它 |
|---|---|---|
| SQLite | 单文件嵌入式 | 本地开发、个人项目、原型验证、工具脚本。零配置 |
| MySQL | 最流行关系库 | 生产主力、团队项目、中小规模、生态资料最全 |
| PostgreSQL | 功能最全的关系库 | 需要高级特性(JSON/全文检索/地理/GIS)、数据严谨场景 |
开发阶段建议 :先用 SQLite 快速把逻辑跑通,再用一个环境变量切换生产库------ORM 的价值之一就是帮你屏蔽底层方言差异,让你前期不必锁死数据库。
三、SQLAlchemy 的两层 API:Core 和 ORM,先搞懂为什么分两层
SQLAlchemy 的设计常被忽略却极其重要:它提供两层 API。
┌─────────────────────────────────────────────┐
│ 应用层代码 │
├─────────────────────────────────────────────┤
│ ORM(高层):Model 类 ↔ 表 │ ← 大多数人只在这层
│ Session 管理对象生命周期 │
├─────────────────────────────────────────────┤
│ Core(中层):Table + SQL Expression │ ← 需要精细控制时下来
│ Engine / Connection 直接执行 │
├─────────────────────────────────────────────┤
│ DBAPI + 方言(psycopg2 / pymysql ...) │ ← 最底层
└─────────────────────────────────────────────┘
为什么分两层? 因为"对象关系映射"这个抽象在复杂查询时会漏 (下面第五节细说)。当 ORM 的表达力不够时,你可以降级到 Core 层直接写 SQL Expression ,甚至用原生 SQL------不需要换框架。这就是 SQLAlchemy 比很多"纯 ORM"更耐用的原因:它给你留了一条下探的梯子。
先看最标准的声明式模型(ORM 层):
python
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
engine = create_engine("sqlite:///blog.db", echo=False) # 生产换 mysql+pymysql://...
Base = declarative_base()
class Author(Base):
__tablename__ = "authors"
id = Column(Integer, primary_key=True)
name = Column(String(50), nullable=False)
posts = relationship("Post", back_populates="author")
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
author_id = Column(Integer, ForeignKey("authors.id"))
author = relationship("Author", back_populates="posts")
Base.metadata.create_all(engine) # 自动建表
Session = sessionmaker(bind=engine)
这段声明式代码本身就在回答一个问题:为什么 MyBatis 需要 resultMap,而这里不需要? 因为这里把表结构直接写成了 Python 类,字段即列、关系即 relationship------映射信息是"声明"出来的,框架读类就知道如何映射,不需要一份单独的映射配置。
四、Session:Python ORM 最容易被坑的一环
用过 MyBatis 的人会自然找 sqlSession,但 SQLAlchemy 的 Session 是另一个物种------它不是"一次请求一个连接",而是一个"工作单元(Unit of Work)"的缓存层。 这是新手最容易翻车的地方。
典型坑:Session 的缓存让你以为"写进去了"其实没有。
python
# 错误的直觉:new_author 明明对象上看到了 id
session = Session()
new_author = Author(name="张三")
session.add(new_author)
# 没有 commit()!对象可能还没真正入库,id 可能是临时的
# 正确做法:事务要么提交要么回滚
session = Session()
try:
author = Author(name="张三")
session.add(author)
session.commit() # 真正 flush 到数据库,事务提交
print(author.id) # 现在 id 才是数据库里的真实主键
except Exception:
session.rollback() # 出错回滚,别让脏数据留在事务里
finally:
session.close()
Session 的正确心智: 它管理一个"待提交的变更集合"。add() 只是把对象放进集合,commit() 才真正写库。忘 commit 是 Python ORM 的第一大事故 ,比 MyBatis 忘 session.commit() 更容易发生,因为很多人根本没意识到有个隐式事务在工作。
再提醒一个生命周期坑: 不要把 Session 设为全局单例在多个线程共享------Session 不是线程安全 的。正确做法是每请求/每工作单元一个 Session(配合依赖注入或 contextvar),用完即关。
五、N+1 查询:全自动 ORM 的"隐藏账单"
这是"全自动"最隐蔽的代价。看这段"自然"的代码:
python
session = Session()
authors = session.query(Author).all() # 1 条 SQL 查所有作者
for author in authors: # ⚠️ 下面循环里每条都触发查询
print(author.name, len(author.posts)) # N 条 SQL 查每个作者的 posts
# 总共 1 + N 条 SQL ------ N+1 问题!
ORM 替你"懒加载"了 author.posts------当你真正访问它时才发查询。这个便利在循环里就成了性能黑洞:查 100 个作者就发 101 条 SQL。
解法一(Django 风格):一次性预加载关联 ,MyBatis 也有类似的 <collection> 联表或两步查。
python
from sqlalchemy.orm import joinedload
authors = session.query(Author).options(
joinedload(Author.posts) # 用 JOIN 一次性把 posts 一起查出来
).all()
解法二:显式用 Core/原生 SQL 联表,绕过 ORM 的懒加载。
关键认知: ORM 的懒加载在"单个对象取关联"时很方便,但在"循环里批量取关联 "时是灾难。这个反模式在 MyBatis、Hibernate、SQLAlchemy、Django ORM 里全都会发生------因为它们是同一个问题:抽象层替你做的"聪明事",在你没意识到时变成隐藏账单。
定位手段 :给引擎开 SQL 日志(echo=True 或用 logging 打印 SQL),一旦发现循环里 SQL 数量暴涨,基本就是 N+1。
六、Django ORM 对比:链式查询与 select_related
如果你走 Django 路线,它的 ORM 是另一套风格------链式 QuerySet:
python
# Django ORM:链式、惰性求值
from django.contrib.auth.models import User
active_admins = (
User.objects
.filter(is_active=True, is_staff=True) # 条件
.select_related("profile") # 外键预加载(对应 joinedload)
.order_by("-date_joined") # 排序
[:10] # 切片 = LIMIT
)
# 惰性:上面只是构建 QuerySet,真正取值时才发 SQL
for user in active_admins:
print(user.username, user.profile.bio)
对应关系记忆:
- Django 的
select_related(外键,JOIN 预取)≈ SQLAlchemy 的joinedload - Django 的
prefetch_related(多对多/反向,二次查询)≈ SQLAlchemy 的selectinload - 两者都在解决同一个 N+1 问题,只是 API 名字不同。
Django ORM 与 SQLAlchemy 的取舍 :Django ORM 和 Django 全家桶深度绑定、语法糖多、上手快,适合"跟着 Django 一条龙";SQLAlchemy 更独立、更贴近 SQL、两层 API 更灵活,适合 FastAPI/Flask 的"自由组装"路线。没有谁绝对好,取决于你的框架选型。
小结:MyBatis 老手眼中的 Python ORM 速查
| 我的 MyBatis 经验 | 对应到 Python ORM |
|---|---|
sqlSession + commit |
Session + commit(注意隐式事务) |
resultMap 手写映射 |
声明式 Model 类自动映射 |
<collection> / 两步查 |
joinedload / select_related |
| SQL 尽在掌握 | Core 层 / 原生 SQL 兜底 |
| Mapper 接口 + XML | Model 类 + Query / QuerySet |
| 数据库选型 | SQLite(dev) / MySQL / PostgreSQL |
核心洞察收个尾: MyBatis 的"半自动"把 SQL 控制权留给你,代价是你得手写映射;Python ORM 的"全自动"替你写 SQL,代价是你得理解 Session/懒加载这些隐式机制,否则会踩 N+1、忘 commit 的坑。抽象泄漏定律在这里的体现是:越"自动"的层,越需要你懂它"漏"出来的那部分------这就是为什么高手既会用 ORM,又能随时下探到 SQL。