You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:04:38