symfony2 查询中间件通过 doctrine 事件与自定义 repository 封装实现轻量控制:统一处理查询上下文、缓存(主键/固定条件自动键生成+版本号失效)、n+1 开发提示、超时熔断(5秒超时+3次降级)。

直接在 Symfony2 查询流程中插入中间件,不是靠改 Doctrine 或写装饰器,而是利用 Doctrine 的事件系统 + 自定义 Repository 封装层。它不拦截 SQL,而是统一控制查询前后的上下文、缓存策略和性能监控,真正轻量又可控。
用 Doctrine 事件监听查询生命周期
Doctrine 提供了 onKernelRequest 和原生的 postGenerateSchema 等钩子,但对“每次查询”做干预,最稳的方式是监听 preSelect 和 postSelect 事件(需启用 SQL Logger 或自定义 Connection Wrapper)。更实用的做法是:在 Repository 基类里统一注入行为。
- 创建抽象基类
BaseRepository,所有 Repository 继承它 - 在
find()、findBy()等方法开头记录开始时间、参数、实体名 - 执行完后计算耗时,若超过 200ms 自动记录到日志或 StatsD(如:
app.db.query.slow.user) - 避免在事件监听器里做 DB 操作(如查配置表),防止递归触发自身
按场景自动启用查询缓存
不是所有查询都适合缓存。中间件要能识别“可缓存模式”:主键查询(find($id))、固定条件单条(findOneBy(['slug' => $s]))、只读静态列表(如状态字典)。
- 对
find()类查询,生成缓存键如entity:User:123,TTL 设为 3600 秒 - 对
findBy(),仅当条件数组键名固定、值为标量且不含 null/对象时才缓存,键为query:User:status=active&role=admin - 缓存失效不走删除,而用版本号:在
parameters.yml中配cache_version_user: 2,键中拼入该值,更新时只需改数字 - 禁用 Doctrine 默认的 Query Cache(易与二级缓存冲突),专注用 APCu 或 Redis 存结果数组
拦截 N+1 并抛出开发提示
线上不报错,但开发环境必须立刻暴露问题。中间件可在查询返回前检查是否触发了懒加载代理(Proxy 对象未初始化)。
- 在 Controller 返回前,遍历响应数据,对每个实体调用
$em->getUnitOfWork()->getEntityState($entity) === UnitOfWork::STATE_MANAGED - 若发现某实体的关联属性是未初始化 Proxy(如
$user->getPosts()是UserProxy实例),且当前请求是dev环境,则抛出DebugNPlusOneException - 异常消息明确指出哪一行模板或服务触发了额外查询,并建议改用
JOIN或addSelect - 生产环境跳过此检查,只记录慢查询中的关联字段访问频次
查询超时与熔断保护
数据库偶尔慢不可怕,连续慢会拖垮整个 PHP 进程。中间件需在底层连接上设硬性约束。
- 修改
app/config/config.yml中 Doctrine 配置,添加:options: { PDO::ATTR_TIMEOUT: 5 }(单位秒) - 封装一个
SafeQueryExecutor,包装executeQuery(),捕获PDOException中含"SQLSTATE[HY000]: General error"或超时关键词 - 连续 3 次查询超时,自动切换至降级逻辑:返回缓存旧数据 + HTTP header
X-DB-Fallback: true - 降级状态维持 60 秒,期间所有同类型查询跳过 DB,直接走缓存或空数组











