Предположим, что у меня есть объекты Product
объекты Review
прикрепленные к продуктам. Можно ли прикрепить поля к объекту Product
на основе некоторого результата, возвращаемого SQL-запросом? Подобно прикреплению поля ReviewsCount
равного COUNT(Reviews.ID) as ReviewsCount
.
Я знаю, что это можно сделать в такой функции, как
public function getReviewsCount() { return count($this->Reviews); }
Но я хочу сделать это с помощью SQL, чтобы свести к минимуму количество запросов к базе данных и повысить производительность, так как обычно мне может не понадобиться загружать сотни отзывов, но все равно нужно знать номер. Я думаю, что запуск COUNT
SQL будет намного быстрее, чем через 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();
Затем в вашем объекте Product у вас будет поле $reviewsCount
и счет будет сопоставлен с этим. Обратите внимание, что это будет работать только в том случае, если у вас есть столбец, определенный в мясной таблице Doctrine, например:
/** * @ORM\Column(type="integer") */ private $reviewsCount; public function getReviewsCount() { return $this->reviewsCount; }
Это то, что предлагает документация Aggregate Fields Doctrine. Проблема заключается в том, что вы, по сути дела, делаете Doctrine, reviews_count
, что у вас есть другой столбец в вашей базе данных, называемый reviews_count
, чего вы не хотите. Таким образом, это будет работать без физического добавления этого столбца, но если вы когда-нибудь запустите doctrine:schema:update
она добавит этот столбец для вас. К сожалению, Doctrine действительно не разрешает виртуальные свойства, поэтому другим решением было бы написать собственный пользовательский гидратор или, возможно, подписаться на событие loadClassMetadata
и вручную добавить отображение самостоятельно после загрузки вашей конкретной сущности (или сущностей).
Обратите внимание, что если вы делаете что-то вроде COUNT(r.id) AS reviewsCount
вы больше не сможете использовать COUNT(id)
в своей функции addFieldResult()
и вместо этого должны использовать alias reviewsCount
для этого второго параметра.
Вы также можете использовать ResultSetMappingBuilder
в качестве начала использования сопоставления набора результатов.
Мое фактическое предложение состоит в том, чтобы сделать это вручную вместо того, чтобы проходить через все эти дополнительные вещи. По существу создайте обычный запрос, который возвращает как вашу сущность, так и скалярные результаты в массив, затем установите скалярный результат в соответствующее, неотображенное поле на вашей сущности и верните объект.
После подробного расследования я обнаружил, что есть несколько способов сделать что-то близкое к тому, что я хотел, включая перечисленные в других ответах, но все они имеют некоторые минусы. Наконец, я решил использовать 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');
Надеюсь, что это может помочь кому-то 🙂
Да, возможно, вам нужно использовать 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(); }