Commit 6729ed28 authored by romanb's avatar romanb

[2.0] Implemented DQL bulk UPDATE support for Class Table Inheritance....

[2.0] Implemented DQL bulk UPDATE support for Class Table Inheritance. Corrections to MultiTableDeleteExecutor and SqlWalker. DQL bulk UPDATE support not yet fully complete.
parent 537c8e49
......@@ -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);
}
}
\ No newline at end of file
......@@ -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);
}
}
\ No newline at end of file
......@@ -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;
}
}
\ No newline at end of file
......@@ -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);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment