是否可以将 SQL 函数的结果用作 Doctrine 中的字段?

Posted

技术标签:

【中文标题】是否可以将 SQL 函数的结果用作 Doctrine 中的字段?【英文标题】:Is it possible to use result of an SQL function as a field in Doctrine? 【发布时间】:2016-05-26 08:22:51 【问题描述】:

假设我有 Product 实体和 Review 实体附加到产品。是否可以根据 SQL 查询返回的某些结果将字段附加到 Product 实体?就像附加一个等于COUNT(Reviews.ID) as ReviewsCountReviewsCount 字段。

我知道可以在类似的函数中做到这一点

public function getReviewsCount() 
    return count($this->Reviews);

但我想用 SQL 来最小化数据库查询的数量并提高性能,因为通常我可能不需要加载数百条评论,但仍然需要知道其中的数量。我认为运行 SQL 的 COUNT 比浏览 100 个产品并为每个产品计算 100 条评论要快得多。此外,这只是示例,在实践中我需要更复杂的函数,我认为 mysql 会处理得更快。如果我错了,请纠正我。

【问题讨论】:

我不这么认为。您是否考虑过使用命名查询example? 是的。我在对 Tomasz Madeyski 的回答的评论中注意到了本机查询的问题。问题是它们与实体一起返回标量,而我希望它们成为实体的字段以避免混乱。 【参考方案1】:

以前的答案对我没有帮助,但我找到了一个解决方案:

我的用例不同,所以代码是模拟的。但关键是使用addScalarResult,然后在实体上设置聚合时清理结果。

use Doctrine\ORM\Query\ResultSetMappingBuilder;

// ...

$sql = "
  SELECT p.*, COUNT(r.id) AS reviewCount
  FROM products p 
  LEFT JOIN reviews r ON p.id = r.product_id
";

$em = $this->getEntityManager();
$rsm = new ResultSetMappingBuilder($em, ResultSetMappingBuilder::COLUMN_RENAMING_CUSTOM);
$rsm->addRootEntityFromClassMetadata('App\Entity\Product', 'p');
$rsm->addScalarResult('reviewCount', 'reviewCount');

$query = $em->createNativeQuery($sql, $rsm);
$result = $query->getResult();

// APPEND the aggregated field to the Entities
$aggregatedResult = [];
foreach ($result as $resultItem) 
  $product = $resultItem[0];
  $product->setReviewCount( $resultItem["reviewCount"] );
  array_push($aggregatedResult, $product);


return $aggregatedResult;

【讨论】:

【参考方案2】:

您可以映射single column result to an entity field - 查看native queries 和ResultSetMapping 来实现此目的。举个简单的例子:

use Doctrine\ORM\Query\ResultSetMapping;

$sql = '
    SELECT p.*, COUNT(r.id)
    FROM products p
    LEFT JOIN reviews r ON p.id = r.product_id
';

$rsm = new ResultSetMapping;
$rsm->addEntityResult('AppBundle\Entity\Product', 'p');
$rsm->addFieldResult('p', 'COUNT(id)', 'reviewsCount');

$query   = $this->getEntityManager()->createNativeQuery($sql, $rsm);
$results = $query->getResult();

然后在您的 Product 实体中,您将有一个 $reviewsCount 字段,并且计数将映射到该字段。请注意,这仅在您在 Doctrine 元数据中定义了一个列时才有效,如下所示:

/**
 * @ORM\Column(type="integer")
 */
private $reviewsCount;

public function getReviewsCount()

    return $this->reviewsCount;

这是Aggregate Fields Doctrine 文档所建议的。问题在于,您实际上是在让 Doctrine 认为您的数据库中有另一个名为 reviews_count 的列,这是您不想要的。因此,这仍然可以在不实际添加该列的情况下工作,但如果您曾经运行过doctrine:schema:update,它将为您添加该列。不幸的是,Doctrine 并没有真正允许虚拟属性,因此另一种解决方案是编写您自己的自定义 hydrator,或者订阅 loadClassMetadata 事件并在您的特定实体(或多个实体)加载后自己手动添加映射。

请注意,如果您执行COUNT(r.id) AS reviewsCount 之类的操作,则您不能再在您的addFieldResult() 函数中使用COUNT(id),而必须为第二个参数使用别名reviewsCount

您还可以使用ResultSetMappingBuilder 作为开始使用结果集映射。

我的实际建议是手动执行此操作,而不是通过所有额外的东西。本质上是创建一个普通查询,将您的实体和标量结果返回到一个数组中,然后将标量结果设置为您实体上相应的未映射字段,然后返回实体。

【讨论】:

这可能是一个理想的解决方案,但如果我将结果分配给未链接到列的属性,则会出现“未定义索引:ReviewsCount”异常。即使该属性存在并且是公共的。有没有办法让 Doctrine 可以访问属性而不将其分配给数据库列? 大小写必须匹配,所以它必须是 $reviewsCount 而不是 $ReviewsCount 我明白了。它匹配。然而,只有当我为该字段分配一个数据库列时它才有效(即使我不做教义:模式:更新)。但即使在这种情况下,如果我使用不同的查询获得相同的实体 - 它会覆盖重置此字段的更改,尽管我将这些结果分开保存。看起来学说缓存结果并返回相同的实体,但重新加载了字段并重置了 reviewsCount 字段。 你能把你有的东西发一下吗?因为它应该与您的 COUNT() 列一起使用,而不仅仅是物理数据库列。 我只是按照你写的来做。博客应用程序:帖子而不是产品和评论而不是评论。结果 id 相同:“注意:未定义索引:CommentsCount”。【参考方案3】:

经过详细调查,我发现有几种方法可以做一些接近我想要的事情,包括在其他答案中列出的,但它们都有一些缺点。最后我决定使用CustomHydrators。似乎不使用 ORM 管理的属性无法使用 ResultSetMapping 作为字段进行映射,但可以作为标量获取并手动附加到实体(因为 php 允许动态附加对象属性)。但是,您从学说中获得的结果仍保留在缓存中。这意味着如果您进行包含这些实体的其他查询,则可能会重置以这种方式设置的属性。

另一种方法是将这些字段直接添加到学说的元数据缓存中。我尝试在 CustomHydrator 中这样做:

protected function getClassMetadata($className)

    if ( ! isset($this->_metadataCache[$className])) 
        $this->_metadataCache[$className] = $this->_em->getClassMetadata($className);

        if ($className === "SomeBundle\Entity\Product") 
            $this->insertField($className, "ReviewsCount");
        
    

    return $this->_metadataCache[$className];


protected function insertField($className, $fieldName) 
    $this->_metadataCache[$className]->fieldMappings[$fieldName] = ["fieldName" => $fieldName, "type" => "text", "scale" => 0, "length" => null, "unique" => false, "nullable" => true, "precision" => 0];
    $this->_metadataCache[$className]->reflFields[$fieldName] = new \ReflectionProperty($className, $fieldName);

    return $this->_metadataCache[$className];

但是,该方法也存在重置实体属性的问题。所以,我的最终解决方案就是使用 stdClass 来获得相同的结构,但不受教义管理:

namespace SomeBundle;

use PDO;
use Doctrine\ORM\Query\ResultSetMapping;

class CustomHydrator extends \Doctrine\ORM\Internal\Hydration\ObjectHydrator 
    public function hydrateAll($stmt, $resultSetMapping, array $hints = array()) 
        $data = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $result = [];

        foreach($resultSetMapping->entityMappings as $root => $something) 
            $rootIDField = $this->getIDFieldName($root, $resultSetMapping);

            foreach($data as $row) 
                $key = $this->findEntityByID($result, $row[$rootIDField]);

                if ($key === null) 
                    $result[] = new \stdClass();
                    end($result);
                    $key = key($result);
                

                foreach ($row as $column => $field)
                    if (isset($resultSetMapping->columnOwnerMap[$column]))
                        $this->attach($result[$key], $field, $this->getPath($root, $resultSetMapping, $column));
            
        


        return $result;
    

    private function getIDFieldName($entityAlias, ResultSetMapping $rsm) 
        foreach ($rsm->fieldMappings as $key => $field)
            if ($field === 'ID' && $rsm->columnOwnerMap[$key] === $entityAlias) return $key;

            return null;
    

    private function findEntityByID($array, $ID) 
        foreach($array as $index => $entity)
            if (isset($entity->ID) && $entity->ID === $ID) return $index;

        return null;
    

    private function getPath($root, ResultSetMapping $rsm, $column) 
        $path = [$rsm->fieldMappings[$column]];
        if ($rsm->columnOwnerMap[$column] !== $root) 
            array_splice($path, 0, 0, $this->getParent($root, $rsm, $rsm->columnOwnerMap[$column]));

        return $path;
    

    private function getParent($root, ResultSetMapping $rsm, $entityAlias) 
        $path = [];
        if (isset($rsm->parentAliasMap[$entityAlias])) 
            $path[] = $rsm->relationMap[$entityAlias];
            array_splice($path, 0, 0, $this->getParent($root, $rsm, array_search($rsm->parentAliasMap[$entityAlias], $rsm->relationMap)));
        

        return $path;
    

    private function attach($object, $field, $place) 
        if (count($place) > 1) 
            $prop = $place[0];
            array_splice($place, 0, 1);
            if (!isset($object->$prop)) $object->$prop = new \stdClass();
            $this->attach($object->$prop, $field, $place);
         else 
            $prop = $place[0];
            $object->$prop = $field;
        
    

使用该类,您可以获得任何结构并附加任何您喜欢的实体:

$sql = '
    SELECT p.*, COUNT(r.id)
    FROM products p
    LEFT JOIN reviews r ON p.id = r.product_id
';

$em = $this->getDoctrine()->getManager();

$rsm = new ResultSetMapping();
$rsm->addEntityResult('SomeBundle\Entity\Product', 'p');
$rsm->addFieldResult('p', 'COUNT(id)', 'reviewsCount');

$query = $em->createNativeQuery($sql, $rsm);

$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'SomeBundle\CustomHydrator');
$results = $query->getResult('CustomHydrator');

希望对某人有所帮助:)

【讨论】:

【参考方案4】:

如果您使用 Doctrine 2.1+,请考虑使用EXTRA_LAZY associations:

它们允许您在实体中实现类似您的方法,直接计算关联而不是检索其中的所有实体:

/**
* @ORM\OneToMany(targetEntity="Review", mappedBy="Product" fetch="EXTRA_LAZY")
*/
private $Reviews;

public function getReviewsCount() 
    return $this->Reviews->count();

【讨论】:

看起来不错,但仍然不是我需要的。正如我所提到的,在实践中计算要复杂一些。就像我需要知道正面评论和负面评论的数量一样,如果不加载所有集合并通过它,这似乎是不可能的。【参考方案5】:

是的,有可能,您需要使用QueryBuilder 来实现:

$result = $em->getRepository('AppBundle:Product')
    ->createQueryBuilder('p')
    ->select('p, count(r.id) as countResult')
    ->leftJoin('p.Review', 'r')
    ->groupBy('r.id')
    ->getQuery()
    ->getArrayResult();

现在您可以执行以下操作:

foreach ($result as $row) 
    echo $row['countResult'];
    echo $row['anyOtherProductField'];

【讨论】:

问题是在这种情况下我得到一个标量,与实体分开。如果所有这些标量都附加到同一个实体,这可能不是问题,但是如果它们应该附加到不同的实体怎么办?比如我想选择产品和评论,对于产品我想有评论的数量,而对于评论我想有喜欢和不喜欢的数量。

以上是关于是否可以将 SQL 函数的结果用作 Doctrine 中的字段?的主要内容,如果未能解决你的问题,请参考以下文章

可以将 Oracle-Sql 中的函数用作 DML 吗?

我可以将 C++17 无捕获 lambda constexpr 转换运算符的结果用作函数指针模板非类型参数吗?

在自定义函数中使用 SQL 查询作为数组参数(输入)

将对象用作远程 SQL Server Reporting Services 报表的数据源

函数调用的结果可以用作默认参数值吗?

游标是不是将 SELECT 结果记录集存储在内存中?