. */ namespace Doctrine\ORM\Query\Exec; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Types\Type; use Doctrine\ORM\Query\ParameterTypeInferer; use Doctrine\ORM\Query\AST; /** * Executes the SQL statements for bulk DQL UPDATE statements on classes in * Class Table Inheritance (JOINED). * * @author Roman Borschel * @since 2.0 */ class MultiTableUpdateExecutor extends AbstractSqlExecutor { /** * @var string */ private $_createTempTableSql; /** * @var string */ private $_dropTempTableSql; /** * @var string */ private $_insertSql; /** * @var array */ private $_sqlParameters = array(); /** * @var int */ private $_numParametersInUpdateClause = 0; /** * Initializes a new MultiTableUpdateExecutor. * * @param \Doctrine\ORM\Query\AST\Node $AST The root AST node of the DQL query. * @param \Doctrine\ORM\Query\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) { $em = $sqlWalker->getEntityManager(); $conn = $em->getConnection(); $platform = $conn->getDatabasePlatform(); $quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); $updateClause = $AST->updateClause; $primaryClass = $sqlWalker->getEntityManager()->getClassMetadata($updateClause->abstractSchemaName); $rootClass = $em->getClassMetadata($primaryClass->rootEntityName); $updateItems = $updateClause->updateItems; $tempTable = $platform->getTemporaryTableName($rootClass->getTemporaryIdTableName()); $idColumnNames = $rootClass->getIdentifierColumnNames(); $idColumnList = implode(', ', $idColumnNames); // 1. Create an INSERT INTO temptable ... SELECT identifiers WHERE $AST->getWhereClause() $sqlWalker->setSQLTableAlias($primaryClass->getTableName(), 't0', $updateClause->aliasIdentificationVariable); $this->_insertSql = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ')' . ' SELECT t0.' . implode(', t0.', $idColumnNames); $rangeDecl = new AST\RangeVariableDeclaration($primaryClass->name, $updateClause->aliasIdentificationVariable); $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); $updateSql = 'UPDATE ' . $quoteStrategy->getTableName($class, $platform) . ' SET '; foreach ($updateItems as $updateItem) { $field = $updateItem->pathExpression->field; if (isset($class->fieldMappings[$field]) && ! isset($class->fieldMappings[$field]['inherited']) || isset($class->associationMappings[$field]) && ! isset($class->associationMappings[$field]['inherited'])) { $newValue = $updateItem->newValue; if ( ! $affected) { $affected = true; ++$i; } else { $updateSql .= ', '; } $updateSql .= $sqlWalker->walkUpdateItem($updateItem); //FIXME: parameters can be more deeply nested. traverse the tree. //FIXME (URGENT): With query cache the parameter is out of date. Move to execute() stage. if ($newValue instanceof AST\InputParameter) { $parameterName = $newValue->name; $parameter = $sqlWalker->getQuery()->getParameter($parameterName); $value = $sqlWalker->getQuery()->processParameterValue($parameter->getValue()); $type = ($parameter->getValue() === $value) ? $parameter->getType() : ParameterTypeInferer::inferType($value); $this->_sqlParameters[$i]['parameters'][] = $value; $this->_sqlParameters[$i]['types'][] = $type; ++$this->_numParametersInUpdateClause; } } } if ($affected) { $this->_sqlStatements[$i] = $updateSql . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')'; } } // Append WHERE clause to insertSql, if there is one. if ($AST->whereClause) { $this->_insertSql .= $sqlWalker->walkWhereClause($AST->whereClause); } // 4. Store DDL for temporary identifier table. $columnDefinitions = array(); foreach ($idColumnNames as $idColumnName) { $columnDefinitions[$idColumnName] = array( 'notnull' => true, 'type' => Type::getType($rootClass->getTypeOfColumn($idColumnName)) ); } $this->_createTempTableSql = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' (' . $platform->getColumnDeclarationListSQL($columnDefinitions) . ')'; $this->_dropTempTableSql = $platform->getDropTemporaryTableSQL($tempTable); } /** * {@inheritDoc} */ public function execute(Connection $conn, array $params, array $types) { $numUpdated = 0; // Create temporary id table $conn->executeUpdate($this->_createTempTableSql); try { // Insert identifiers. Parameters from the update clause are cut off. $numUpdated = $conn->executeUpdate( $this->_insertSql, array_slice($params, $this->_numParametersInUpdateClause), array_slice($types, $this->_numParametersInUpdateClause) ); // Execute UPDATE statements for ($i=0, $count=count($this->_sqlStatements); $i<$count; ++$i) { $parameters = array(); $types = array(); if (isset($this->_sqlParameters[$i])) { $parameters = isset($this->_sqlParameters[$i]['parameters']) ? $this->_sqlParameters[$i]['parameters'] : array(); $types = isset($this->_sqlParameters[$i]['types']) ? $this->_sqlParameters[$i]['types'] : array(); } $conn->executeUpdate($this->_sqlStatements[$i], $parameters, $types); } } catch (\Exception $exception) { // FAILURE! Drop temporary table to avoid possible collisions $conn->executeUpdate($this->_dropTempTableSql); // Re-throw exception throw $exception; } // Drop temporary table $conn->executeUpdate($this->_dropTempTableSql); return $numUpdated; } }