DQL插入QueryBuilder遇'c'作用域错误,如何实现DRY?
我在做一个应用的统计页面,里面包含数十项计算,为了避免代码重复,尝试把带条件的DQL插入到QueryBuilder时,遇到了错误:Error: 'c' is used outside the scope of its declaration。
涉及的基础实体是Household和Contact,计算逻辑基于联系日期范围、联系站点(site)及联系类型(type),我已经有服务来生成where子句和查询参数数组。
已知所有代码放在单个函数里时计算能正常运行,问题似乎出在Contact实体的关联及其必要约束上。想请教下这种场景下能不能实现DRY(Don't Repeat Yourself)原则?
以下代码都在Household实体的仓库中:
现有DQL代码
private function reportHousehold($criteria) { return $this->createQueryBuilder('i') ->select('i.id') ->join('TruckeeProjectmanaBundle:Contact', 'c', 'WITH', 'c.household = i') ->where($criteria['betweenWhereClause']) ->andWhere($criteria['siteWhereClause']) ->andWhere($criteria['contactWhereClause']) ->getDQL() ; }
$criteria示例:
$criteria['betweenWhereClause'] = 'c.contactDate BETWEEN :startDate AND :endDate'
其中一项计算的代码
public function res($criteria) { $parameters = array_merge( $criteria['betweenParameters'], $criteria['siteParameters'], $criteria['startParameters'], $criteria['startParameters'], $criteria['contactParameters']); $qb = $this->getEntityManager()->createQueryBuilder(); return $this->getEntityManager()->createQueryBuilder() ->select('h.id, 12*(YEAR(:startDate) - h.arrivalyear) + (MONTH(:startDate) - h.arrivalmonth) Mos') ->from('TruckeeProjectmanaBundle:Household', 'h') ->distinct() // 这里插入DQL时出错: ->where($qb->expr()->in('h.id', $this->reportHousehold($criteria))) ->andWhere($qb->expr()->isNotNull('h.arrivalyear')) ->andWhere($qb->expr()->isNotNull('h.arrivalmonth')) ->andWhere($criteria['startWhereClause']) ->setParameters($parameters) ->getQuery()->getResult() ; }
当然可以实现DRY原则!问题出在你直接把子查询的DQL字符串插入主查询时,子查询里的别名c只在子查询的作用域内有效,但主查询的表达式解析器会误以为c是主查询的别名,导致作用域错误。我们可以换一种方式,复用QueryBuilder实例而不是拼接DQL字符串,这样Doctrine会自动处理别名作用域的问题。
方法1:封装子查询为QueryBuilder实例
修改reportHousehold方法,让它返回QueryBuilder而不是DQL字符串:
private function reportHouseholdQueryBuilder($criteria) { return $this->createQueryBuilder('i') ->select('i.id') ->join('TruckeeProjectmanaBundle:Contact', 'c', 'WITH', 'c.household = i') ->where($criteria['betweenWhereClause']) ->andWhere($criteria['siteWhereClause']) ->andWhere($criteria['contactWhereClause']); }
然后在主查询里直接传入这个QueryBuilder实例到in表达式中:
public function res($criteria) { $parameters = array_merge( $criteria['betweenParameters'], $criteria['siteParameters'], $criteria['startParameters'], $criteria['contactParameters']); // 注意你之前重复了startParameters,这里去掉了重复项 $subQb = $this->reportHouseholdQueryBuilder($criteria); return $this->createQueryBuilder('h') // 直接用仓库的createQueryBuilder更简洁 ->select('h.id, 12*(YEAR(:startDate) - h.arrivalyear) + (MONTH(:startDate) - h.arrivalmonth) Mos') ->distinct() ->where($this->expr()->in('h.id', $subQb)) ->andWhere($this->expr()->isNotNull('h.arrivalyear')) ->andWhere($this->expr()->isNotNull('h.arrivalmonth')) ->andWhere($criteria['startWhereClause']) ->setParameters($parameters) ->getQuery()->getResult() ; }
Doctrine会自动处理子查询的别名作用域,不会再出现'c' is used outside the scope的错误,同时你也复用了子查询的逻辑,完全符合DRY原则。
方法2:封装通用的约束逻辑
如果你的多个统计查询都需要用到这些Contact相关的约束,还可以进一步把约束逻辑封装成一个单独的方法,用来修改传入的QueryBuilder:
private function addContactConstraints(QueryBuilder $qb, $householdAlias, $criteria) { // $householdAlias是主查询中Household的别名,比如'i'或'h' $qb->join('TruckeeProjectmanaBundle:Contact', 'c', 'WITH', "c.household = $householdAlias") ->andWhere($criteria['betweenWhereClause']) ->andWhere($criteria['siteWhereClause']) ->andWhere($criteria['contactWhereClause']); return $qb; }
然后在需要的地方调用这个方法:
// 子查询场景 private function reportHouseholdQueryBuilder($criteria) { return $this->addContactConstraints($this->createQueryBuilder('i'), 'i', $criteria) ->select('i.id'); } // 直接在主查询中使用(如果不需要子查询的话) public function anotherStatistic($criteria) { $parameters = array_merge( $criteria['betweenParameters'], $criteria['siteParameters'], $criteria['contactParameters'] ); $qb = $this->createQueryBuilder('h') ->select('COUNT(DISTINCT h.id)') ->andWhere($this->expr()->isNotNull('h.arrivalyear')); $this->addContactConstraints($qb, 'h', $criteria); return $qb->setParameters($parameters)->getQuery()->getSingleScalarResult(); }
这种方式更灵活,不管是子查询还是主查询里需要关联Contact并添加约束,都可以复用这段逻辑。
额外注意点
- 你之前的
$parameters里重复了$criteria['startParameters'],记得去掉重复项,避免参数覆盖或者冗余。 - 如果你的
$criteria里的where子句使用了命名参数,确保所有参数在主查询里都能正确设置,Doctrine会自动处理子查询的参数合并(只要参数名不冲突)。
内容的提问来源于stack exchange,提问作者geoB

