Flask 中如何使用 SQLAlchemy?数据库连接和 Session 要注意什么?
简化版
Flask 常通过 Flask-SQLAlchemy 集成 SQLAlchemy。工程里通常先创建 db = SQLAlchemy(),再在应用工厂中 db.init_app(app)。数据库 Session 要按请求生命周期管理,请求结束后释放连接;查询时要警惕 N+1、事务边界、连接池配置和模型层过度耦合。
详细版
典型结构:
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://..."
db.init_app(app)
return app
模型:
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64))
常见注意点:
- 不要在模块导入时直接绑定全局 app,优先
init_app。 - 请求结束要正确清理 Session,Flask-SQLAlchemy 会集成上下文管理。
- 写操作要明确 commit/rollback。
- 避免 N+1 查询,合理使用 joinedload、selectinload 等。
- 生产配置连接池、超时、回收策略。
完整版教学
一、Flask 为什么常搭配 SQLAlchemy
Flask 核心不内置 ORM,这和 Django 不同。Flask 项目如果需要关系型数据库,常见选择是 SQLAlchemy。SQLAlchemy 是 Python 生态中成熟的数据库工具,既支持 ORM,也支持 Core 表达式。
Flask-SQLAlchemy 是对 SQLAlchemy 的 Flask 集成,提供配置读取、应用上下文绑定、模型基类、Session 管理等便利能力。它不是另一个 ORM,而是让 SQLAlchemy 更自然地融入 Flask 项目。
面试时要说清:Flask 不内置 ORM,但可以通过 Flask-SQLAlchemy 使用 SQLAlchemy;这体现了 Flask “核心轻、扩展补齐”的设计。
二、扩展初始化方式
工程里推荐这样写:
# extensions.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
在应用工厂中绑定:
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
return app
不要在 extensions.py 中直接创建 app 并绑定 db。否则测试和多环境配置会很麻烦,也容易循环导入。init_app 的延迟绑定方式更适合 Flask 应用工厂模式。
三、Session 和数据库连接的关系
SQLAlchemy 的 Session 不是 Flask Session,它是 ORM 工作单元,负责跟踪对象状态、发 SQL、提交事务。不要把数据库 Session 和用户登录 Session 混淆。
一次请求中,你可能查询、修改多个模型对象,最后 commit:
user = User(name="Tom")
db.session.add(user)
db.session.commit()
如果发生异常,应该 rollback:
try:
db.session.commit()
except Exception:
db.session.rollback()
raise
Flask-SQLAlchemy 会结合应用上下文管理 scoped session,请求结束时移除 Session,释放连接回连接池。但业务层仍然要清楚事务提交和回滚语义。
四、事务边界和 commit 位置
常见坏习惯是在很多 helper 函数里随手 db.session.commit()。这样会导致事务边界分散,调用方无法控制一组操作是否原子。
更好的方式是:底层函数负责 add、update、delete,业务服务或视图的上层负责统一 commit。比如创建订单时,订单、明细、库存变化要么一起成功,要么一起失败。事务边界应该围绕业务一致性设计,而不是围绕单个模型方法随手提交。
如果使用 Flask-SQLAlchemy,也可以结合上下文管理或自定义事务装饰器,但核心原则不变:commit 位置要可控,异常时要 rollback。
五、查询性能:N+1 和加载策略
ORM 最常见性能坑是 N+1。比如查询用户列表后循环访问每个用户的订单:
users = User.query.all()
for user in users:
print(user.orders)
如果 orders 是懒加载关系,这可能触发大量额外 SQL。SQLAlchemy 可以使用加载策略优化:
from sqlalchemy.orm import selectinload
users = User.query.options(selectinload(User.orders)).all()
对于多对一或一对一关系,可以考虑 joinedload;对于一对多集合,selectinload 常常更稳。具体选择要看数据规模、分页、重复行和 SQL 复杂度。
六、生产连接池配置
生产环境还要关注连接池。数据库连接不是无限资源,如果连接泄露或池配置不合理,会出现连接耗尽、请求阻塞。
常见配置包括连接池大小、连接回收时间、连接超时等。不同数据库和部署方式配置不同,但面试时可以说明:数据库连接由 SQLAlchemy engine/pool 管理,请求结束应释放连接,长事务和连接泄露都会影响并发能力。
在多进程 Gunicorn 部署下,每个 worker 都可能维护自己的连接池。因此总连接数大约是 worker 数 × 每个进程连接池规模,不能只看单进程配置。
七、模型层设计边界
Flask 项目里模型不应该变成所有业务逻辑的大杂烩。简单字段、关系、少量领域方法可以放模型;复杂流程更适合 service 层。否则模型会越来越胖,测试也困难。
API 项目还要注意模型和序列化分离。不要直接把 ORM 对象随便 JSON 化,应该通过 schema、serializer 或明确转换函数控制输出字段,避免暴露内部字段。
八、常见误区与追问
| 概念 | 正确理解 | 常见风险 |
|---|---|---|
| SQLAlchemy Session | ORM 工作单元和事务上下文 | 和用户 Session 混淆 |
commit() | 提交当前事务 | 在底层函数到处提交导致边界混乱 |
| 连接池 | 复用数据库连接 | worker 数叠加后连接数超限 |
- 误区:数据库 Session 和 Flask 用户 Session 是一回事。 前者管理 ORM 对象、事务和连接,后者保存用户会话状态,二者语义完全不同。
- 误区:每个模型方法里都可以随手 commit。 commit 应围绕完整业务事务边界设计,底层函数随手提交会让回滚和一致性变困难。
- 误区:用了 ORM 就不会有慢查询。 ORM 仍会生成 SQL,N+1、缺索引、加载过多字段、长事务都会造成性能问题。
- 追问:Flask-SQLAlchemy 为什么常用 init_app? 它支持扩展对象先创建、后绑定 app,适合应用工厂和多环境测试。
- 追问:如何解决一对多列表的 N+1? 可根据关系和分页情况考虑
selectinload、joinedload等加载策略,并观察生成 SQL。 - 追问:Gunicorn 多 worker 下连接池怎么算? 总连接消耗约等于 worker 数乘以每个进程可能持有的连接数,要和数据库最大连接数一起估算。
记忆钩子:Flask-SQLAlchemy 面试别只讲模型定义,要讲清“延迟初始化、事务边界、N+1、连接池”四件事。
九、加强记忆
Flask 常用 Flask-SQLAlchemy 集成 SQLAlchemy。重点不是会写模型,而是理解扩展延迟初始化、ORM Session 与事务边界、请求结束资源释放、N+1 查询优化和生产连接池配置。数据库 Session 不是用户 Session,commit/rollback 要围绕业务一致性设计。