是否可以将SQL函数的结果用作Doctrine中的字段?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了是否可以将SQL函数的结果用作Doctrine中的字段?相关的知识,希望对你有一定的参考价值。
假设我有Product
实体和附加到产品的Review
实体。是否可以根据SQL查询返回的结果将字段附加到Product
实体?喜欢附加ReviewsCount
字段等于COUNT(Reviews.ID) as ReviewsCount
。
我知道可以在像这样的函数中做到这一点
public function getReviewsCount() {
return count($this->Reviews);
}
但是我想用SQL来做这件事来最小化数据库查询的数量并提高性能,因为通常我可能不需要加载数百个评论,但仍然需要知道数量。我认为运行SQL的COUNT
比通过100个产品并计算100个评论要快得多。而且,这只是一个例子,在实践中我需要更复杂的功能,我认为mysql会更快地处理。如我错了请纠正我。
你可以映射一个single column result to an entity field - 看看native queries和ResultSetMapping来实现这个目的。举个简单的例子:
use DoctrineORMQueryResultSetMapping;
$sql = '
SELECT p.*, COUNT(r.id)
FROM products p
LEFT JOIN reviews r ON p.id = r.product_id
';
$rsm = new ResultSetMapping;
$rsm->addEntityResult('AppBundleEntityProduct', 'p');
$rsm->addFieldResult('p', 'COUNT(id)', 'reviewsCount');
$query = $this->getEntityManager()->createNativeQuery($sql, $rsm);
$results = $query->getResult();
然后在您的Product实体中,您将拥有一个$reviewsCount
字段,并且计数将映射到该字段。请注意,只有在Doctrine元数据中定义了列时才会这样做,如下所示:
/**
* @ORMColumn(type="integer")
*/
private $reviewsCount;
public function getReviewsCount()
{
return $this->reviewsCount;
}
这是Aggregate Fields Doctrine文档的建议。问题在于你基本上是在让Doctrine认为你的数据库中有另一个名为reviews_count
的列,这是你不想要的。所以,这仍然可以在没有物理添加该列的情况下工作,但是如果您运行了doctrine:schema:update
,它将为您添加该列。不幸的是,Doctrine并不真正允许虚拟属性,因此另一种解决方案是编写自己的自定义水槽,或者订阅loadClassMetadata
事件并在您的特定实体(或实体)加载后自己手动添加映射。
请注意,如果您执行COUNT(r.id) AS reviewsCount
之类的操作,那么您不能再在COUNT(id)
函数中使用addFieldResult()
,而必须使用别名reviewsCount
作为第二个参数。
您还可以使用ResultSetMappingBuilder
作为开始使用结果集映射。
我的实际建议是手动完成,而不是通过所有额外的东西。基本上创建一个普通查询,将实体和标量结果返回到数组中,然后将标量结果设置为实体上相应的未映射字段,并返回实体。
经过详细的调查后,我发现有几种方法可以做一些接近我想要的事情,包括在其他答案中列出,但所有这些都有一些缺点。最后我决定使用CustomHydrators。似乎没有使用ORM管理的属性不能用ResultSetMapping作为字段进行映射,但可以作为标量获取并手动附加到实体(因为php允许动态附加对象属性)。但是,从doctrine获得的结果仍然在缓存中。这意味着如果您进行其他包含这些实体的查询,则可能会重置以这种方式设置的属性。
另一种方法是将这些字段直接添加到doctrine的元数据缓存中。我尝试在CustomHydrator中执行此操作:
protected function getClassMetadata($className)
{
if ( ! isset($this->_metadataCache[$className])) {
$this->_metadataCache[$className] = $this->_em->getClassMetadata($className);
if ($className === "SomeBundleEntityProduct") {
$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来获得相同的结构,但不是由doctrine管理的:
namespace SomeBundle;
use PDO;
use DoctrineORMQueryResultSetMapping;
class CustomHydrator extends DoctrineORMInternalHydrationObjectHydrator {
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('SomeBundleEntityProduct', 'p');
$rsm->addFieldResult('p', 'COUNT(id)', 'reviewsCount');
$query = $em->createNativeQuery($sql, $rsm);
$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'SomeBundleCustomHydrator');
$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 associations:
它们允许您在实体中实现类似于您的方法,直接计算关联而不是检索其中的所有实体:
/**
* @ORMOneToMany(targetEntity="Review", mappedBy="Product" fetch="EXTRA_LAZY")
*/
private $Reviews;
public function getReviewsCount() {
return $this->Reviews->count();
}
以上是关于是否可以将SQL函数的结果用作Doctrine中的字段?的主要内容,如果未能解决你的问题,请参考以下文章
Doctrine - 是不是可以将列结果与其他列结果而不是她的列名相关联
是否可以在 Doctrine 2 ODM 中的多个字段上使用 sort()?