[2.0] Implemented DQL bulk UPDATE support for Class Table Inheritance. Corrections to MultiTableDeleteExecutor and SqlWalker. DQL bulk UPDATE support not yet fully complete.
This commit is contained in:
parent
537c8e4951
commit
6729ed28e7
9 changed files with 176 additions and 44 deletions
|
@ -31,9 +31,8 @@ use Doctrine\ORM\Mapping\AssociationMapping;
|
|||
* That means, if the collection is part of a many-many mapping and you remove
|
||||
* entities from the collection, only the links in the relation table are removed (on flush).
|
||||
* Similarly, if you remove entities from a collection that is part of a one-many
|
||||
* mapping this will only result in the nulling out of the foreign keys on flush
|
||||
* (or removal of the links in the relation table if the one-many is mapped through a
|
||||
* relation table). If you want entities in a one-many collection to be removed when
|
||||
* mapping this will only result in the nulling out of the foreign keys on flush.
|
||||
* If you want entities in a one-many collection to be removed when
|
||||
* they're removed from the collection, use deleteOrphans => true on the one-many
|
||||
* mapping.
|
||||
*
|
||||
|
|
|
@ -30,7 +30,8 @@ use Doctrine\ORM\Mapping\ClassMetadata;
|
|||
use Doctrine\ORM\Events;
|
||||
|
||||
/**
|
||||
* Base class for all EntityPersisters.
|
||||
* Base class for all EntityPersisters. An EntityPersister is a class that knows
|
||||
* how to persist (and to some extent how to load) entities of a specific type.
|
||||
*
|
||||
* @author Roman Borschel <roman@code-factory.org>
|
||||
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
||||
|
@ -237,15 +238,6 @@ class StandardEntityPersister
|
|||
return $this->_class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the table name to use for temporary identifier tables of the class
|
||||
* persisted by this persister.
|
||||
*/
|
||||
public function getTemporaryIdTableName()
|
||||
{
|
||||
return $this->_class->primaryTable['name'] . '_id_tmp';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the data changeset of an entity for database insertion.
|
||||
* The array that is passed as the second parameter is filled with
|
||||
|
|
|
@ -59,8 +59,8 @@ class RangeVariableDeclaration extends Node
|
|||
return $this->_classMetadata;
|
||||
}
|
||||
|
||||
public function dispatch($sqlWalker)
|
||||
public function dispatch($walker)
|
||||
{
|
||||
return $sqlWalker->walkRangeVariableDeclaration($this);
|
||||
return $walker->walkRangeVariableDeclaration($this);
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@ namespace Doctrine\ORM\Query\AST;
|
|||
|
||||
/**
|
||||
* UpdateItem ::= [IdentificationVariable "."] {StateField | SingleValuedAssociationField} "=" NewValue
|
||||
* NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
|
||||
* EnumPrimary | SimpleEntityExpression | "NULL"
|
||||
*
|
||||
* @author robo
|
||||
*/
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
namespace Doctrine\ORM\Query\Exec;
|
||||
|
||||
/**
|
||||
* Doctrine_ORM_Query_QueryResult
|
||||
* Base class for SQL statement executors.
|
||||
*
|
||||
* @author Roman Borschel <roman@code-factory.org>
|
||||
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
||||
|
@ -72,15 +72,20 @@ abstract class AbstractExecutor implements \Serializable
|
|||
$primaryClass = $sqlWalker->getEntityManager()->getClassMetadata(
|
||||
$AST->getDeleteClause()->getAbstractSchemaName()
|
||||
);
|
||||
|
||||
if ($primaryClass->isInheritanceTypeJoined()) {
|
||||
return new MultiTableDeleteExecutor($AST, $sqlWalker);
|
||||
} else {
|
||||
return new SingleTableDeleteUpdateExecutor($AST, $sqlWalker);
|
||||
}
|
||||
} else if ($isUpdateStatement) {
|
||||
//TODO: Check whether to pick MultiTableUpdateExecutor instead
|
||||
return new SingleTableDeleteUpdateExecutor($AST, $sqlWalker);
|
||||
$primaryClass = $sqlWalker->getEntityManager()->getClassMetadata(
|
||||
$AST->getUpdateClause()->getAbstractSchemaName()
|
||||
);
|
||||
if ($primaryClass->isInheritanceTypeJoined()) {
|
||||
return new MultiTableUpdateExecutor($AST, $sqlWalker);
|
||||
} else {
|
||||
return new SingleTableDeleteUpdateExecutor($AST, $sqlWalker);
|
||||
}
|
||||
} else {
|
||||
return new SingleSelectExecutor($AST, $sqlWalker);
|
||||
}
|
||||
|
@ -103,4 +108,5 @@ abstract class AbstractExecutor implements \Serializable
|
|||
{
|
||||
$this->_sqlStatements = unserialize($serialized);
|
||||
}
|
||||
|
||||
}
|
|
@ -21,6 +21,8 @@
|
|||
|
||||
namespace Doctrine\ORM\Query\Exec;
|
||||
|
||||
use Doctrine\ORM\Query\AST;
|
||||
|
||||
/**
|
||||
* Executes the SQL statements for bulk DQL DELETE statements on classes in
|
||||
* Class Table Inheritance (JOINED).
|
||||
|
@ -42,8 +44,10 @@ class MultiTableDeleteExecutor extends AbstractExecutor
|
|||
*
|
||||
* @param Node $AST The root AST node of the DQL query.
|
||||
* @param SqlWalker $sqlWalker The walker used for SQL generation from the AST.
|
||||
* @internal Any SQL construction and preparation takes place in the constructor for
|
||||
* best performance. With a query cache the executor will be cached.
|
||||
*/
|
||||
public function __construct(\Doctrine\ORM\Query\AST\Node $AST, $sqlWalker)
|
||||
public function __construct(AST\Node $AST, $sqlWalker)
|
||||
{
|
||||
$em = $sqlWalker->getEntityManager();
|
||||
$conn = $em->getConnection();
|
||||
|
@ -51,20 +55,23 @@ class MultiTableDeleteExecutor extends AbstractExecutor
|
|||
$primaryClass = $sqlWalker->getEntityManager()->getClassMetadata(
|
||||
$AST->getDeleteClause()->getAbstractSchemaName()
|
||||
);
|
||||
$primaryDqlAlias = $AST->getDeleteClause()->getAliasIdentificationVariable();
|
||||
$rootClass = $em->getClassMetadata($primaryClass->rootEntityName);
|
||||
|
||||
$tempTable = $rootClass->getTemporaryIdTableName();
|
||||
$idColumnNames = $rootClass->getIdentifierColumnNames();
|
||||
$idColumnList = implode(', ', $idColumnNames);
|
||||
|
||||
// 1. Create a INSERT INTO temptable ... VALUES ( SELECT statement where the SELECT statement
|
||||
// selects the identifiers and uses the WhereClause of the $AST ).
|
||||
// 1. Create an INSERT INTO temptable ... SELECT identifiers WHERE $AST->getWhereClause()
|
||||
$this->_insertSql = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ')'
|
||||
. ' SELECT ' . $idColumnList . ' FROM ' . $conn->quoteIdentifier($rootClass->primaryTable['name']) . ' t0';
|
||||
. ' SELECT t0.' . implode(', t0.', $idColumnNames);
|
||||
$sqlWalker->setSqlTableAlias($primaryClass->primaryTable['name'] . $primaryDqlAlias, 't0');
|
||||
$rangeDecl = new AST\RangeVariableDeclaration($primaryClass, $primaryDqlAlias);
|
||||
$fromClause = new AST\FromClause(array(new AST\IdentificationVariableDeclaration($rangeDecl, null, array())));
|
||||
$this->_insertSql .= $sqlWalker->walkFromClause($fromClause);
|
||||
|
||||
// Append WHERE clause, if there is one.
|
||||
if ($AST->getWhereClause()) {
|
||||
$sqlWalker->setSqlTableAlias($rootClass->primaryTable['name'] . $AST->getDeleteClause()->getAliasIdentificationVariable(), 't0');
|
||||
$this->_insertSql .= $sqlWalker->walkWhereClause($AST->getWhereClause());
|
||||
}
|
||||
|
||||
|
@ -94,10 +101,10 @@ class MultiTableDeleteExecutor extends AbstractExecutor
|
|||
}
|
||||
|
||||
/**
|
||||
* Executes all sql statements.
|
||||
* Executes all SQL statements.
|
||||
*
|
||||
* @param Doctrine\DBAL\Connection $conn The database connection that is used to execute the queries.
|
||||
* @param array $params The parameters.
|
||||
* @param array $params The parameters.
|
||||
* @override
|
||||
*/
|
||||
public function execute(\Doctrine\DBAL\Connection $conn, array $params)
|
||||
|
@ -108,15 +115,11 @@ class MultiTableDeleteExecutor extends AbstractExecutor
|
|||
$conn->exec($this->_createTempTableSql);
|
||||
|
||||
// Insert identifiers
|
||||
$conn->exec($this->_insertSql, $params);
|
||||
$numDeleted = $conn->exec($this->_insertSql, $params);
|
||||
|
||||
// Execute DELETE statements
|
||||
for ($i=0, $count=count($this->_sqlStatements); $i<$count; ++$i) {
|
||||
if ($i == $count-1) {
|
||||
$numDeleted = $conn->exec($this->_sqlStatements[$i]);
|
||||
} else {
|
||||
$conn->exec($this->_sqlStatements[$i]);
|
||||
}
|
||||
foreach ($this->_sqlStatements as $sql) {
|
||||
$conn->exec($sql);
|
||||
}
|
||||
|
||||
// Drop temporary table
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
|
||||
namespace Doctrine\ORM\Query\Exec;
|
||||
|
||||
use Doctrine\ORM\Query\AST;
|
||||
|
||||
/**
|
||||
* Executes the SQL statements for bulk DQL UPDATE statements on classes in
|
||||
* Class Table Inheritance (JOINED).
|
||||
|
@ -30,17 +32,103 @@ namespace Doctrine\ORM\Query\Exec;
|
|||
* @link http://www.doctrine-project.org
|
||||
* @since 2.0
|
||||
* @version $Revision$
|
||||
* @todo For a good implementation that uses temporary tables see the Hibernate sources:
|
||||
* (org.hibernate.hql.ast.exec.MultiTableUpdateExecutor).
|
||||
*/
|
||||
class MultiTableUpdateExecutor extends AbstractExecutor
|
||||
{
|
||||
public function __construct($AST)
|
||||
private $_createTempTableSql;
|
||||
private $_dropTempTableSql;
|
||||
private $_insertSql;
|
||||
private $_sqlParameters = array();
|
||||
private $_numParametersInUpdateClause = 0;
|
||||
|
||||
/**
|
||||
* Initializes a new <tt>MultiTableUpdateExecutor</tt>.
|
||||
*
|
||||
* @param Node $AST The root AST node of the DQL query.
|
||||
* @param SqlWalker $sqlWalker The walker used for SQL generation from the AST.
|
||||
* @internal Any SQL construction and preparation takes place in the constructor for
|
||||
* best performance. With a query cache the executor will be cached.
|
||||
*/
|
||||
public function __construct(AST\Node $AST, $sqlWalker)
|
||||
{
|
||||
// TODO: Inspect the AST, create the necessary SQL queries and store them
|
||||
// in $this->_sqlStatements
|
||||
$em = $sqlWalker->getEntityManager();
|
||||
$conn = $em->getConnection();
|
||||
|
||||
$primaryClass = $sqlWalker->getEntityManager()->getClassMetadata(
|
||||
$AST->getUpdateClause()->getAbstractSchemaName()
|
||||
);
|
||||
$rootClass = $em->getClassMetadata($primaryClass->rootEntityName);
|
||||
|
||||
$updateItems = $AST->getUpdateClause()->getUpdateItems();
|
||||
|
||||
$tempTable = $rootClass->getTemporaryIdTableName();
|
||||
$idColumnNames = $rootClass->getIdentifierColumnNames();
|
||||
$idColumnList = implode(', ', $idColumnNames);
|
||||
|
||||
// 1. Create an INSERT INTO temptable ... SELECT identifiers WHERE $AST->getWhereClause()
|
||||
$this->_insertSql = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ')'
|
||||
. ' SELECT t0.' . implode(', t0.', $idColumnNames);
|
||||
$sqlWalker->setSqlTableAlias($primaryClass->primaryTable['name'] . $AST->getUpdateClause()->getAliasIdentificationVariable(), 't0');
|
||||
$rangeDecl = new AST\RangeVariableDeclaration($primaryClass, $AST->getUpdateClause()->getAliasIdentificationVariable());
|
||||
$fromClause = new AST\FromClause(array(new AST\IdentificationVariableDeclaration($rangeDecl, null, array())));
|
||||
$this->_insertSql .= $sqlWalker->walkFromClause($fromClause);
|
||||
|
||||
// 2. Create ID subselect statement used in UPDATE ... WHERE ... IN (subselect)
|
||||
$idSubselect = 'SELECT ' . $idColumnList . ' FROM ' . $tempTable;
|
||||
|
||||
// 3. Create and store UPDATE statements
|
||||
$classNames = array_merge($primaryClass->parentClasses, array($primaryClass->name), $primaryClass->subClasses);
|
||||
$i = -1;
|
||||
foreach (array_reverse($classNames) as $className) {
|
||||
$affected = false;
|
||||
$class = $em->getClassMetadata($className);
|
||||
$tableName = $class->primaryTable['name'];
|
||||
$updateSql = 'UPDATE ' . $conn->quoteIdentifier($tableName) . ' SET ';
|
||||
|
||||
foreach ($updateItems as $updateItem) {
|
||||
$field = $updateItem->getField();
|
||||
if (isset($class->fieldMappings[$field]) && ! isset($class->fieldMappings[$field]['inherited'])) {
|
||||
$newValue = $updateItem->getNewValue();
|
||||
if ( ! $affected) {
|
||||
$affected = true;
|
||||
++$i;
|
||||
} else {
|
||||
$updateSql .= ', ';
|
||||
}
|
||||
$updateSql .= $sqlWalker->walkUpdateItem($updateItem);
|
||||
//FIXME: parameters can be more deeply nested. traverse the tree.
|
||||
if ($newValue instanceof AST\InputParameter) {
|
||||
$paramKey = $newValue->isNamed() ? $newValue->getName() : $newValue->getPosition();
|
||||
$this->_sqlParameters[$i][] = $sqlWalker->getQuery()->getParameter($paramKey);
|
||||
++$this->_numParametersInUpdateClause;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($affected) {
|
||||
$this->_sqlStatements[$i] = $updateSql . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')';
|
||||
}
|
||||
}
|
||||
|
||||
// Append WHERE clause to insertSql, if there is one.
|
||||
if ($AST->getWhereClause()) {
|
||||
$this->_insertSql .= $sqlWalker->walkWhereClause($AST->getWhereClause());
|
||||
}
|
||||
|
||||
// 4. Store DDL for temporary identifier table.
|
||||
$columnDefinitions = array();
|
||||
foreach ($idColumnNames as $idColumnName) {
|
||||
$columnDefinitions[$idColumnName] = array(
|
||||
'notnull' => true,
|
||||
'type' => \Doctrine\DBAL\Types\Type::getType($rootClass->getTypeOfColumn($idColumnName))
|
||||
);
|
||||
}
|
||||
$this->_createTempTableSql = 'CREATE TEMPORARY TABLE ' . $tempTable . ' ('
|
||||
. $conn->getDatabasePlatform()->getColumnDeclarationListSql($columnDefinitions)
|
||||
. ', PRIMARY KEY(' . $idColumnList . '))';
|
||||
$this->_dropTempTableSql = 'DROP TABLE ' . $tempTable;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes all sql statements.
|
||||
*
|
||||
|
@ -50,6 +138,22 @@ class MultiTableUpdateExecutor extends AbstractExecutor
|
|||
*/
|
||||
public function execute(\Doctrine\DBAL\Connection $conn, array $params)
|
||||
{
|
||||
//...
|
||||
$numUpdated = 0;
|
||||
|
||||
// Create temporary id table
|
||||
$conn->exec($this->_createTempTableSql);
|
||||
|
||||
// Insert identifiers. Parameters from the update clause are cut off.
|
||||
$numUpdated = $conn->exec($this->_insertSql, array_slice($params, $this->_numParametersInUpdateClause));
|
||||
|
||||
// Execute UPDATE statements
|
||||
for ($i=0, $count=count($this->_sqlStatements); $i<$count; ++$i) {
|
||||
$conn->exec($this->_sqlStatements[$i], $this->_sqlParameters[$i]);
|
||||
}
|
||||
|
||||
// Drop temporary table
|
||||
$conn->exec($this->_dropTempTableSql);
|
||||
|
||||
return $numUpdated;
|
||||
}
|
||||
}
|
|
@ -83,6 +83,16 @@ class SqlWalker implements TreeWalker
|
|||
$this->_parserResult = $parserResult;
|
||||
$this->_queryComponents = $queryComponents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Query instance used by the walker.
|
||||
*
|
||||
* @return Query.
|
||||
*/
|
||||
public function getQuery()
|
||||
{
|
||||
return $this->_query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Connection used by the walker.
|
||||
|
@ -703,6 +713,9 @@ class SqlWalker implements TreeWalker
|
|||
*/
|
||||
public function walkUpdateItem($updateItem)
|
||||
{
|
||||
$useTableAliasesBefore = $this->_useSqlTableAliases;
|
||||
$this->_useSqlTableAliases = false;
|
||||
|
||||
$sql = '';
|
||||
$dqlAlias = $updateItem->getIdentificationVariable();
|
||||
$qComp = $this->_queryComponents[$dqlAlias];
|
||||
|
@ -723,6 +736,8 @@ class SqlWalker implements TreeWalker
|
|||
$sql .= $this->_conn->quote($newValue);
|
||||
}
|
||||
}
|
||||
|
||||
$this->_useSqlTableAliases = $useTableAliasesBefore;
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
@ -1175,14 +1190,19 @@ class SqlWalker implements TreeWalker
|
|||
$qComp = $this->_queryComponents[$dqlAlias];
|
||||
$class = $qComp['metadata'];
|
||||
|
||||
if ($numParts > 2) {
|
||||
/*if ($numParts > 2) {
|
||||
for ($i = 1; $i < $numParts-1; ++$i) {
|
||||
//TODO
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
if ($this->_useSqlTableAliases) {
|
||||
$sql .= $this->getSqlTableAlias($class->getTableName() . $dqlAlias) . '.';
|
||||
if ($class->isInheritanceTypeJoined() && isset($class->fieldMappings[$fieldName]['inherited'])) {
|
||||
$sql .= $this->getSqlTableAlias($this->_em->getClassMetadata(
|
||||
$class->fieldMappings[$fieldName]['inherited'])->getTableName() . $dqlAlias) . '.';
|
||||
} else {
|
||||
$sql .= $this->getSqlTableAlias($class->getTableName() . $dqlAlias) . '.';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($class->associationMappings[$fieldName])) {
|
||||
|
|
|
@ -67,9 +67,15 @@ class ClassTableInheritanceTest extends \Doctrine\Tests\OrmFunctionalTestCase
|
|||
$this->_em->clear();
|
||||
|
||||
//TODO: Test bulk UPDATE
|
||||
$query = $this->_em->createQuery("update Doctrine\Tests\Models\Company\CompanyEmployee p set p.name = ?1, p.department = ?2 where p.name='Guilherme Blanco' and p.salary = ?3");
|
||||
$query->setParameter(1, 'NewName');
|
||||
$query->setParameter(2, 'NewDepartment');
|
||||
$query->setParameter(3, 100000);
|
||||
$query->getSql();
|
||||
$numUpdated = $query->execute();
|
||||
$this->assertEquals(1, $numUpdated);
|
||||
|
||||
$query = $this->_em->createQuery("delete from Doctrine\Tests\Models\Company\CompanyPerson p");
|
||||
|
||||
$numDeleted = $query->execute();
|
||||
$this->assertEquals(2, $numDeleted);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue