简体   繁体   English

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

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

Assume I have Product entities and Review entities attached to products.假设我有附加到产品的Product实体和Review实体。 Is it possible to attach a fields to a Product entity based on some result returned by an SQL query?是否可以根据 SQL 查询返回的某些结果将字段附加到Product实体? Like attaching a ReviewsCount field equal to COUNT(Reviews.ID) as ReviewsCount .就像附加一个等于COUNT(Reviews.ID) as ReviewsCountReviewsCount字段COUNT(Reviews.ID) as ReviewsCount

I know it is possible to do that in a function like我知道可以在像这样的函数中做到这一点

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

But I want doing this with SQL to minimize number of database queries and increase performance, as normally I may not need to load hundreds of reviews, but still need to know there number.但我想用 SQL 来做到这一点,以最大限度地减少数据库查询的数量并提高性能,因为通常我可能不需要加载数百条评论,但仍然需要知道那里的数量。 I think running SQL's COUNT would be much faster than going through 100 Products and calculating 100 Reviews for each.我认为运行 SQL 的COUNT比浏览 100 个产品并为每个产品计算 100 条评论要快得多。 Moreover, that is just example, on practice I need more complex functions, that I think MySQL would process faster.此外,这只是示例,在实践中我需要更复杂的函数,我认为 MySQL 会处理得更快。 Correct me if I'm wrong.如果我错了纠正我。

You can map a single column result to an entity field - look at native queries and ResultSetMapping to achieve this.您可以将单列结果映射到实体字段- 查看本机查询ResultSetMapping以实现此目的。 As a simple example:作为一个简单的例子:

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

Then in your Product entity you would have a $reviewsCount field and the count would be mapped to that.然后在您的 Product 实体中,您将有一个$reviewsCount字段,并且计数将映射到该字段。 Note that this will only work if you have a column defined in the Doctrine metadata, like so:请注意,这仅适用于在 Doctrine 元数据中定义的列,如下所示:

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

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

This is what is suggested by the Aggregate Fields Doctrine documentation.这是Aggregate Fields Doctrine 文档所建议的。 The problem is here is that you are essentially making Doctrine think you have another column in your database called reviews_count , which is what you don't want.问题在于,您实际上是在让 Doctrine 认为您的数据库中有另一列名为reviews_count ,这是您不想要的。 So, this will still work without physically adding that column, but if you ever run a doctrine:schema:update it's going to add that column in for you.所以,这仍然可以在不实际添加该列的情况下工作,但是如果您运行过doctrine:schema:update它将为您添加该列。 Unfortunately Doctrine does not really allow virtual properties, so another solution would be to write your own custom hydrator, or perhaps subscribe to the loadClassMetadata event and manually add the mapping yourself after your particular entity (or entities) load.不幸的是,Doctrine 并没有真正允许虚拟属性,因此另一种解决方案是编写您自己的自定义 hydrator,或者订阅loadClassMetadata事件并在您的特定实体(或多个实体)加载后手动添加映射。

Note that if you do something like COUNT(r.id) AS reviewsCount then you can no longer use COUNT(id) in your addFieldResult() function, and must instead use the alias reviewsCount for that second parameter.请注意,如果您执行COUNT(r.id) AS reviewsCount则您不能再在addFieldResult()函数中使用COUNT(id) ,而必须为第二个参数使用别名reviewsCount

You can also use the ResultSetMappingBuilder as a start into using the result set mapping.您还可以使用ResultSetMappingBuilder作为开始使用结果集映射。

My actual suggestion is to do this manually instead of going through all of that extra stuff.我的实际建议是手动执行此操作,而不是完成所有额外的工作。 Essentially create a normal query that returns both your entity and scalar results into an array, then set the scalar result to a corresponding, unmapped field on your entity, and return the entity.本质上创建一个普通查询,将实体和标量结果返回到数组中,然后将标量结果设置为实体上相应的未映射字段,并返回实体。

After detailed investigation I've found there are several ways to do something close to what I wanted including listed in other answers, but all of them have some minuses.经过详细调查,我发现有几种方法可以做一些接近我想要的事情,包括在其他答案中列出,但所有这些都有一些缺点。 Finally I've decided to use CustomHydrators .最后我决定使用CustomHydrators It seems that properties not managed with ORM cannot be mapped with ResultSetMapping as fields, but can be got as scalars and attached to an entity manually (as PHP allows to attach object properties on the fly).似乎不使用 ORM 管理的属性不能用 ResultSetMapping 作为字段映射,但可以作为标量获取并手动附加到实体(因为 PHP 允许动态附加对象属性)。 However, result that you get from doctrine remains in the cache.但是,您从学说中获得的结果仍保留在缓存中。 That means properties set in that way may be reset if you make some other query that would contain these entities too.这意味着如果您进行其他一些包含这些实体的查询,以这种方式设置的属性可能会被重置。

Another way to do that was adding these field directly to doctrine's metadata cache.另一种方法是将这些字段直接添加到学说的元数据缓存中。 I tried doing that in a CustomHydrator:我尝试在 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];
}

However, that method also had problems with entities' properties reset.但是,该方法也存在重置实体属性的问题。 So, my final solution was just to use stdClass to get the same structure, but not managed by doctrine:所以,我的最终解决方案只是使用 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;
        }
    }
}

With that class you can get any structure and attach any entities however you like:使用该类,您可以获得任何结构并附加任何您喜欢的实体:

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

Hope that may help someone :)希望可以帮助某人:)

Yes, it is possible, you need to use QueryBuilder to achieve that:是的,有可能,您需要使用QueryBuilder来实现:

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

and now you can do something like:现在您可以执行以下操作:

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

If you're on Doctrine 2.1+, consider using EXTRA_LAZY associations :如果您使用的是 Doctrine 2.1+,请考虑使用EXTRA_LAZY 关联

They allow you to implement a method like yours in your entity, doing a straight count on the association instead of retrieving all the entities in it:它们允许您在实体中实现类似您的方法,直接对关联进行计数,而不是检索其中的所有实体:

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

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

The previous answers didn't help me, but I found a solution doing the following:以前的答案对我没有帮助,但我找到了执行以下操作的解决方案:

My use case was different so the code is a mock.我的用例不同,所以代码是一个模拟。 But the key is to use addScalarResult and then cleanup the result while setting the aggregate on the entity.但关键是使用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;

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM