Doctrine原生SQL查询必须使用Connection而非EntityManager,因后者仅支持DQL和简单条件构建;复杂SQL需通过$em->getConnection()获取Connection实例,用参数绑定、ResultSetMapping映射结果,并手动管理事务。

Doctrine原生SQL查询必须用Connection,不能用EntityManager
Doctrine的EntityManager只支持DQL(类似SQL的面向对象查询语言)和简单条件构建器,写复杂SQL(如多表JOIN、子查询、窗口函数、UNION)时,EntityManager->createQuery()会直接报错或生成错误语句。必须降级到数据库连接层。
正确入口是EntityManager::getConnection(),它返回Connection实例,提供executeStatement()(INSERT/UPDATE/DELETE)、fetchOne()、fetchAllAssociative()等原生方法:
$conn = $entityManager->getConnection();
$result = $conn->fetchAllAssociative(
'SELECT u.name, COUNT(p.id) as post_count
FROM user u
LEFT JOIN post p ON p.user_id = u.id
GROUP BY u.id
HAVING COUNT(p.id) > ?',
[5]
);
- 参数绑定必须用
?占位符,不支持命名参数(如:limit),除非显式调用prepare()+bindValue() - 返回结果默认是索引数组,
fetchAllAssociative()才返回键名关联数组 - 别在
WHERE里拼接变量——这是SQL注入高发区,一律用参数绑定
需要映射到实体?用ResultSetMapping手动定义字段与属性关系
原生SQL查出来的数据不会自动转成Entity对象,即使字段名和实体属性一致。想复用实体逻辑(如getter、序列化、验证),就得用ResultSetMapping告诉Doctrine“哪一列对应哪个属性”。
例如把上面的统计结果映射到UserSummary DTO(不是Entity,但结构清晰):
$rsm = new ResultSetMapping();
$rsm->addScalarResult('name', 'name');
$rsm->addScalarResult('post_count', 'postCount');
$sql = 'SELECT u.name, COUNT(p.id) as post_count ...';
$nativeQuery = $em->createNativeQuery($sql, $rsm);
$result = $nativeQuery->getResult(); // 返回 ['name' => 'Alice', 'postCount' => 12]
-
addScalarResult()第一个参数是SQL中AS后的别名,第二个是结果数组的键名 - 如果真要映射到Entity类(比如
User),得用addEntityResult()+addFieldResult(),但要求SQL字段名必须严格匹配实体字段(包括大小写),且主键字段必须存在并命名为id - DTO比Entity更安全——避免触发Doctrine的变更追踪、生命周期回调等副作用
复杂分页别用LIMIT/OFFSET,改用游标分页或子查询优化
Doctrine自带的Paginator对原生SQL支持极弱,OFFSET在大数据量下性能陡降,而count(*)全表扫描更是雪上加霜。尤其当SQL含GROUP BY、JOIN或窗口函数时,count(*)根本没法套用。
- 优先用游标分页:基于上一页最后一条记录的排序字段值(如
created_at+id)做条件过滤,SQL里加WHERE (created_at, id) > (?, ?) - 若必须总数,单独写精简的COUNT查询,只SELECT COUNT(1),去掉所有JOIN和计算字段
- 避免在原生SQL里写
LIMIT :limit OFFSET :offset然后丢给Paginator——它会尝试重写SQL,大概率失败或漏数据
事务里执行原生SQL,记得用Connection::beginTransaction()而非EntityManager::beginTransaction()
很多人以为$em->beginTransaction()能包住原生SQL,其实它只控制EntityManager内部的UoW(Unit of Work),对Connection层无影响。混合使用DQL和原生SQL时,事务不一致会导致部分写入成功、部分回滚失败。
- 统一用
$conn->beginTransaction()开始,$conn->commit()或$conn->rollback()结束 - 如果同时操作Entity(如
$em->persist())和原生SQL,确保所有操作都在同一个Connection事务内,且EntityManager未提前flush - 调用
$em->flush()前,先检查$conn->isTransactionActive(),避免“事务已提交但Entity还没刷”的状态错乱
原生SQL的灵活性是以放弃ORM抽象为代价的,每个绑定、每行映射、每次事务控制都得亲手捏住——漏掉任意一个,轻则数据错乱,重则死锁或注入。


















