1
0
mirror of synced 2024-12-12 22:36:02 +03:00

Merge pull request #885 from bakura10/criteria-many-to-many

Add support for ManyToMany Criteria
This commit is contained in:
Guilherme Blanco 2014-03-16 11:33:10 -05:00
commit 44c1dae1b9
7 changed files with 173 additions and 32 deletions

View File

@ -20,6 +20,7 @@
namespace Doctrine\ORM\Cache\Persister;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Persisters\CollectionPersister;
@ -272,4 +273,12 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister
{
return $this->persister->slice($collection, $offset, $length);
}
/**
* {@inheritDoc}
*/
public function loadCriteria(PersistentCollection $collection, Criteria $criteria)
{
return $this->persister->loadCriteria($collection, $criteria);
}
}

View File

@ -869,8 +869,10 @@ final class PersistentCollection implements Collection, Selectable
return $this->coll->matching($criteria);
}
if ($this->association['type'] !== ClassMetadata::ONE_TO_MANY) {
throw new \RuntimeException("Matching Criteria on PersistentCollection only works on OneToMany associations at the moment.");
if ($this->association['type'] === ClassMetadata::MANY_TO_MANY) {
$persister = $this->em->getUnitOfWork()->getCollectionPersister($this->association);
return new ArrayCollection($persister->loadCriteria($this, $criteria));
}
$builder = Criteria::expr();

View File

@ -19,6 +19,7 @@
namespace Doctrine\ORM\Persisters;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\PersistentCollection;
@ -51,7 +52,7 @@ abstract class AbstractCollectionPersister implements CollectionPersister
* @var \Doctrine\DBAL\Platforms\AbstractPlatform
*/
protected $platform;
/**
* The quote strategy.
*
@ -203,6 +204,14 @@ abstract class AbstractCollectionPersister implements CollectionPersister
throw new \BadMethodCallException("Selecting a collection by index is not supported by this CollectionPersister.");
}
/**
* {@inheritdoc}
*/
public function loadCriteria(PersistentCollection $coll, Criteria $criteria)
{
throw new \BadMethodCallException("Filtering a collection by Criteria is not supported by this CollectionPersister.");
}
/**
* Gets the SQL statement used for deleting a row from the collection.
*

View File

@ -19,6 +19,7 @@
namespace Doctrine\ORM\Persisters;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\PersistentCollection;
/**
@ -136,4 +137,14 @@ interface CollectionPersister
* @return mixed
*/
public function get(PersistentCollection $collection, $index);
/**
* Loads association entities matching the given Criteria object.
*
* @param \Doctrine\ORM\PersistentCollection $collection
* @param \Doctrine\Common\Collections\Criteria $criteria
*
* @return array
*/
public function loadCriteria(PersistentCollection $collection, Criteria $criteria);
}

View File

@ -19,8 +19,10 @@
namespace Doctrine\ORM\Persisters;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Query;
use Doctrine\ORM\UnitOfWork;
/**
@ -54,7 +56,7 @@ class ManyToManyPersister extends AbstractCollectionPersister
}
return 'DELETE FROM ' . $tableName
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
}
/**
@ -102,7 +104,7 @@ class ManyToManyPersister extends AbstractCollectionPersister
}
return 'INSERT INTO ' . $joinTable . ' (' . implode(', ', $columns) . ')'
. ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')';
. ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')';
}
/**
@ -178,7 +180,7 @@ class ManyToManyPersister extends AbstractCollectionPersister
}
return 'DELETE FROM ' . $joinTable
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
}
/**
@ -257,7 +259,7 @@ class ManyToManyPersister extends AbstractCollectionPersister
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function slice(PersistentCollection $coll, $offset, $length = null)
{
@ -276,7 +278,7 @@ class ManyToManyPersister extends AbstractCollectionPersister
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function contains(PersistentCollection $coll, $element)
{
@ -302,7 +304,7 @@ class ManyToManyPersister extends AbstractCollectionPersister
}
/**
* {@inheritdoc}
* {@inheritDoc}
*/
public function removeElement(PersistentCollection $coll, $element)
{
@ -485,29 +487,12 @@ class ManyToManyPersister extends AbstractCollectionPersister
return array('', '');
}
$conditions = array();
$association = $mapping;
if ( ! $mapping['isOwningSide']) {
$class = $this->em->getClassMetadata($mapping['targetEntity']);
$association = $class->associationMappings[$mapping['mappedBy']];
}
// A join is needed if there is filtering on the target entity
$tableName = $this->quoteStrategy->getTableName($rootClass, $this->platform);
$joinSql = ' JOIN ' . $tableName . ' te' . ' ON';
$joinColumns = $mapping['isOwningSide']
? $association['joinTable']['inverseJoinColumns']
: $association['joinTable']['joinColumns'];
$tableName = $this->quoteStrategy->getTableName($rootClass, $this->platform);
$joinSql = ' JOIN ' . $tableName . ' te' . ' ON';
$onConditions = $this->getOnConditionSQL($mapping);
foreach ($joinColumns as $joinColumn) {
$joinColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
$refColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
$conditions[] = ' t.' . $joinColumnName . ' = ' . 'te.' . $refColumnName;
}
$joinSql .= implode(' AND ', $conditions);
$joinSql .= implode(' AND ', $onConditions);
return array($joinSql, $filterSql);
}
@ -531,6 +516,108 @@ class ManyToManyPersister extends AbstractCollectionPersister
}
$sql = implode(' AND ', $filterClauses);
return $sql ? "(" . $sql . ")" : "";
return $sql ? '(' . $sql . ')' : '';
}
/**
* Generate ON condition
*
* @param array $mapping
*
* @return array
*/
protected function getOnConditionSQL($mapping)
{
$association = $mapping;
if ( ! $mapping['isOwningSide']) {
$class = $this->em->getClassMetadata($mapping['targetEntity']);
$association = $class->associationMappings[$mapping['mappedBy']];
}
$targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
$joinColumns = $mapping['isOwningSide']
? $association['joinTable']['inverseJoinColumns']
: $association['joinTable']['joinColumns'];
$conditions = array();
foreach ($joinColumns as $joinColumn) {
$joinColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
$refColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
$conditions[] = ' t.' . $joinColumnName . ' = ' . 'te.' . $refColumnName;
}
return $conditions;
}
/**
* {@inheritDoc}
*/
public function loadCriteria(PersistentCollection $coll, Criteria $criteria)
{
$mapping = $coll->getMapping();
$owner = $coll->getOwner();
$ownerMetadata = $this->em->getClassMetadata(get_class($owner));
$whereClauses = $params = array();
foreach ($mapping['relationToSourceKeyColumns'] as $key => $value) {
$whereClauses[] = sprintf('t.%s = ?', $key);
$params[] = $ownerMetadata->getFieldValue($owner, $value);
}
$parameters = $this->expandCriteriaParameters($criteria);
foreach ($parameters as $parameter) {
list($name, $value) = $parameter;
$whereClauses[] = sprintf('te.%s = ?', $name);
$params[] = $value;
}
$mapping = $coll->getMapping();
$targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
$tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform);
$joinTable = $this->quoteStrategy->getJoinTableName($mapping, $ownerMetadata, $this->platform);
$onConditions = $this->getOnConditionSQL($mapping);
$rsm = new Query\ResultSetMappingBuilder($this->em);
$rsm->addRootEntityFromClassMetadata($mapping['targetEntity'], 'te');
$sql = 'SELECT ' . $rsm->generateSelectClause() . ' FROM ' . $tableName . ' te'
. ' JOIN ' . $joinTable . ' t ON'
. implode(' AND ', $onConditions)
. ' WHERE ' . implode(' AND ', $whereClauses);
$stmt = $this->conn->executeQuery($sql, $params);
$hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
return $hydrator->hydrateAll($stmt, $rsm);
}
/**
* Expands Criteria Parameters by walking the expressions and grabbing all
* parameters and types from it.
*
* @param \Doctrine\Common\Collections\Criteria $criteria
*
* @return array
*/
private function expandCriteriaParameters(Criteria $criteria)
{
$expression = $criteria->getWhereExpression();
if ($expression === null) {
return array();
}
$valueVisitor = new SqlValueVisitor();
$valueVisitor->dispatch($expression);
list($values, $types) = $valueVisitor->getParamsAndTypes();
return $types;
}
}

View File

@ -58,6 +58,7 @@ abstract class AbstractCollectionPersisterTest extends OrmTestCase
'removeElement',
'removeKey',
'get',
'loadCriteria'
);
/**
@ -298,4 +299,4 @@ abstract class AbstractCollectionPersisterTest extends OrmTestCase
$this->assertEquals($element, $persister->get($collection, 0));
}
}
}

View File

@ -2,6 +2,7 @@
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Tests\Models\CMS\CmsUser,
Doctrine\Tests\Models\CMS\CmsGroup,
Doctrine\Common\Collections\ArrayCollection;
@ -377,4 +378,25 @@ class ManyToManyBasicAssociationTest extends \Doctrine\Tests\OrmFunctionalTestCa
$user = $this->_em->find(get_class($user), $user->id);
$this->assertEquals(0, count($user->groups));
}
public function testMatching()
{
$user = $this->addCmsUserGblancoWithGroups(2);
$this->_em->clear();
$user = $this->_em->find(get_class($user), $user->id);
$groups = $user->groups;
$this->assertFalse($user->groups->isInitialized(), "Pre-condition: lazy collection");
$criteria = Criteria::create()->where(Criteria::expr()->eq('name', (string) 'Developers_0'));
$result = $groups->matching($criteria);
$this->assertCount(1, $result);
$firstGroup = $result->first();
$this->assertEquals('Developers_0', $firstGroup->name);
$this->assertFalse($user->groups->isInitialized(), "Post-condition: matching does not initialize collection");
}
}