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


Is it possible to use result of an SQL function as a field in Doctrine?

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

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

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

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

您可以将单列结果映射到实体字段 - 查看本机查询和 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();

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

/**
 * @ORM'Column(type="integer")
 */
private $reviewsCount;
public function getReviewsCount()
{
    return $this->reviewsCount;
}

这是聚合字段原则文档所建议的。 这里的问题是,你本质上是让Doctrine认为你的数据库中有另一列叫做reviews_count,这是你不想要的。 因此,这仍然可以在不物理添加该列的情况下工作,但是如果您曾经运行过doctrine:schema:update它将为您添加该列。 不幸的是,Doctrine 并不真正允许虚拟属性,因此另一种解决方案是编写您自己的自定义水化器,或者订阅 loadClassMetadata 事件并在特定实体(或多个实体(加载后自己手动添加映射。

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

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

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

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

另一种方法是将这些字段直接添加到 doctrine 的元数据缓存中。我尝试在自定义补水器中执行此操作:

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');

希望可以帮助某人:)

是的,这是可能的,您需要使用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'];
}

如果您使用的是 Doctrine 2.1+,请考虑使用EXTRA_LAZY关联:

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

/**
* @ORM'OneToMany(targetEntity="Review", mappedBy="Product" fetch="EXTRA_LAZY")
*/
private $Reviews;
public function getReviewsCount() {
    return $this->Reviews->count();
}

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

我的用例不同,所以代码是模拟的。但关键是使用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;