JoinedSubclassPersister.php 19.3 KB
Newer Older
1
<?php
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/*
 *  $Id$
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
19
 * <http://www.doctrine-project.org>.
20 21
 */

22 23
namespace Doctrine\ORM\Persisters;

24
use Doctrine\ORM\ORMException;
25

26
/**
27
 * The joined subclass persister maps a single entity instance to several tables in the
28 29 30 31 32
 * database as it is defined by <tt>Class Table Inheritance</tt>.
 *
 * @author      Roman Borschel <roman@code-factory.org>
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @version     $Revision$
33
 * @link        www.doctrine-project.org
34 35
 * @since       2.0
 */
36 37
class JoinedSubclassPersister extends StandardEntityPersister
{
romanb's avatar
romanb committed
38 39 40 41
    /** Map that maps column names to the table names that own them.
     *  This is mainly a temporary cache, used during a single request.
     */
    private $_owningTableMap = array();
42

romanb's avatar
romanb committed
43 44 45 46 47 48 49 50 51 52 53 54
    /**
     * {@inheritdoc}
     *
     * @override
     */
    protected function _prepareData($entity, array &$result, $isInsert = false)
    {
        parent::_prepareData($entity, $result, $isInsert);
        // Populate the discriminator column
        if ($isInsert) {
            $discColumn = $this->_class->discriminatorColumn;
            $rootClass = $this->_em->getClassMetadata($this->_class->rootEntityName);
55
            $result[$rootClass->primaryTable['name']][$discColumn['name']] = $this->_class->discriminatorValue;
romanb's avatar
romanb committed
56 57
        }
    }
58

59 60 61 62
    /**
     * This function finds the ClassMetadata instance in a inheritance hierarchy
     * that is responsible for enabling versioning.
     *
63
     * @return mixed ClassMetadata instance or false if versioning is not enabled.
64 65 66
     */
    private function _getVersionedClassMetadata()
    {
romanb's avatar
romanb committed
67
        if ($this->_class->isVersioned) {
68 69 70 71 72 73 74 75 76 77 78
            if (isset($this->_class->fieldMappings[$this->_class->versionField]['inherited'])) {
                $definingClassName = $this->_class->fieldMappings[$this->_class->versionField]['inherited'];
                $versionedClass = $this->_em->getClassMetadata($definingClassName);
            } else {
                $versionedClass = $this->_class;
            }
            return $versionedClass;
        }
        return false;
    }

romanb's avatar
romanb committed
79
    /**
80 81
     * Gets the name of the table that owns the column the given field is mapped to.
     * Does only look upwards in the hierarchy, not downwards.
romanb's avatar
romanb committed
82
     *
83 84
     * @param string $fieldName
     * @return string
romanb's avatar
romanb committed
85 86 87 88 89 90 91 92
     * @override
     */
    public function getOwningTable($fieldName)
    {
        if ( ! isset($this->_owningTableMap[$fieldName])) {
            if (isset($this->_class->associationMappings[$fieldName])) {
                if (isset($this->_class->inheritedAssociationFields[$fieldName])) {
                    $this->_owningTableMap[$fieldName] = $this->_em->getClassMetadata(
93 94
                        $this->_class->inheritedAssociationFields[$fieldName]
                        )->primaryTable['name'];
romanb's avatar
romanb committed
95 96 97 98 99
                } else {
                    $this->_owningTableMap[$fieldName] = $this->_class->primaryTable['name'];
                }
            } else if (isset($this->_class->fieldMappings[$fieldName]['inherited'])) {
                $this->_owningTableMap[$fieldName] = $this->_em->getClassMetadata(
100 101
                    $this->_class->fieldMappings[$fieldName]['inherited']
                    )->primaryTable['name'];
romanb's avatar
romanb committed
102 103 104 105 106 107
            } else {
                $this->_owningTableMap[$fieldName] = $this->_class->primaryTable['name'];
            }
        }
        return $this->_owningTableMap[$fieldName];
    }
108

romanb's avatar
romanb committed
109 110 111 112 113 114 115 116 117 118
    /**
     * {@inheritdoc}
     *
     * @override
     */
    public function executeInserts()
    {
        if ( ! $this->_queuedInserts) {
            return;
        }
119

120
        if ($isVersioned = $this->_class->isVersioned) {
121
            $versionedClass = $this->_getVersionedClassMetadata();
122 123
        }

romanb's avatar
romanb committed
124 125 126
        $postInsertIds = array();
        $idGen = $this->_class->idGenerator;
        $isPostInsertId = $idGen->isPostInsertGenerator();
127

romanb's avatar
romanb committed
128 129 130 131 132 133
        // Prepare statement for the root table
        $rootClass = $this->_class->name == $this->_class->rootEntityName ?
                $this->_class : $this->_em->getClassMetadata($this->_class->rootEntityName);
        $rootPersister = $this->_em->getUnitOfWork()->getEntityPersister($rootClass->name);
        $rootTableName = $rootClass->primaryTable['name'];
        $rootTableStmt = $this->_conn->prepare($rootPersister->getInsertSql());
134
        if ($this->_sqlLogger !== null) {
romanb's avatar
romanb committed
135 136 137 138 139 140 141 142 143 144 145
            $sql = array();
            $sql[$rootTableName] = $rootPersister->getInsertSql();
        }
        
        // Prepare statements for sub tables.
        $subTableStmts = array();
        if ($rootClass !== $this->_class) {
            $subTableStmts[$this->_class->primaryTable['name']] = $this->_conn->prepare($this->getInsertSql());
            if ($this->_sqlLogger !== null) {
                $sql[$this->_class->primaryTable['name']] = $this->getInsertSql();
            }
146 147 148
        }
        foreach ($this->_class->parentClasses as $parentClassName) {
            $parentClass = $this->_em->getClassMetadata($parentClassName);
romanb's avatar
romanb committed
149 150 151 152 153 154 155
            $parentTableName = $parentClass->primaryTable['name'];
            if ($parentClass !== $rootClass) {
                $parentPersister = $this->_em->getUnitOfWork()->getEntityPersister($parentClassName);
                $subTableStmts[$parentTableName] = $this->_conn->prepare($parentPersister->getInsertSql());
                if ($this->_sqlLogger !== null) {
                    $sql[$parentTableName] = $parentPersister->getInsertSql();
                }
156
            }
romanb's avatar
romanb committed
157
        }
romanb's avatar
romanb committed
158 159 160 161
        
        // Execute all inserts. For each entity:
        // 1) Insert on root table
        // 2) Insert on sub tables
romanb's avatar
romanb committed
162 163 164
        foreach ($this->_queuedInserts as $entity) {
            $insertData = array();
            $this->_prepareData($entity, $insertData, true);
165

romanb's avatar
romanb committed
166 167
            // Execute insert on root table
            $paramIndex = 1;
168
            if ($this->_sqlLogger !== null) {
romanb's avatar
romanb committed
169 170 171
                $params = array();
                foreach ($insertData[$rootTableName] as $columnName => $value) {
                    $params[$paramIndex] = $value;
romanb's avatar
romanb committed
172
                    $rootTableStmt->bindValue($paramIndex++, $value);
romanb's avatar
romanb committed
173
                }
174
                $this->_sqlLogger->logSql($sql[$rootTableName], $params);
romanb's avatar
romanb committed
175 176
            } else {
                foreach ($insertData[$rootTableName] as $columnName => $value) {
romanb's avatar
romanb committed
177
                    $rootTableStmt->bindValue($paramIndex++, $value);
romanb's avatar
romanb committed
178 179
                }
            }
romanb's avatar
romanb committed
180
            $rootTableStmt->execute();
181

romanb's avatar
romanb committed
182 183 184 185 186 187
            if ($isPostInsertId) {
                $id = $idGen->generate($this->_em, $entity);
                $postInsertIds[$id] = $entity;
            } else {
                $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
            }
188

romanb's avatar
romanb committed
189 190 191 192
            // Execute inserts on subtables.
            // The order doesn't matter because all child tables link to the root table via FK.
            foreach ($subTableStmts as $tableName => $stmt) {
                $data = isset($insertData[$tableName]) ? $insertData[$tableName] : array();
romanb's avatar
romanb committed
193
                $paramIndex = 1;
194
                if ($this->_sqlLogger !== null) {
romanb's avatar
romanb committed
195
                    $params = array();
196
                    foreach ((array) $id as $idVal) {
romanb's avatar
romanb committed
197
                        $params[$paramIndex] = $idVal;
198
                        $stmt->bindValue($paramIndex++, $idVal);
romanb's avatar
romanb committed
199 200 201
                    }
                    foreach ($data as $columnName => $value) {
                        $params[$paramIndex] = $value;
202
                        $stmt->bindValue($paramIndex++, $value);
romanb's avatar
romanb committed
203
                    }
204
                    $this->_sqlLogger->logSql($sql[$tableName], $params);
romanb's avatar
romanb committed
205
                } else {
206 207
                    foreach ((array) $id as $idVal) {
                        $stmt->bindValue($paramIndex++, $idVal);
romanb's avatar
romanb committed
208 209
                    }
                    foreach ($data as $columnName => $value) {
210
                        $stmt->bindValue($paramIndex++, $value);
romanb's avatar
romanb committed
211 212 213 214 215
                    }
                }
                $stmt->execute();
            }
        }
216

romanb's avatar
romanb committed
217 218
        $rootTableStmt->closeCursor();
        foreach ($subTableStmts as $stmt) {
219 220
            $stmt->closeCursor();
        }
221

222 223 224 225
        if ($isVersioned) {
            $this->_assignDefaultVersionValue($versionedClass, $entity, $id);
        }

romanb's avatar
romanb committed
226
        $this->_queuedInserts = array();
227

romanb's avatar
romanb committed
228 229
        return $postInsertIds;
    }
230

romanb's avatar
romanb committed
231 232 233 234 235 236 237 238 239 240
    /**
     * Updates an entity.
     *
     * @param object $entity The entity to update.
     * @override
     */
    public function update($entity)
    {
        $updateData = array();
        $this->_prepareData($entity, $updateData);
241

romanb's avatar
romanb committed
242
        $id = array_combine(
243
            $this->_class->getIdentifierColumnNames(),
244
            $this->_em->getUnitOfWork()->getEntityIdentifier($entity)
romanb's avatar
romanb committed
245
        );
246

247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
        if ($isVersioned = $this->_class->isVersioned) {
            $versionedClass = $this->_getVersionedClassMetadata();
            $versionedTable = $versionedClass->primaryTable['name'];
        }

        if ($updateData) {
            foreach ($updateData as $tableName => $data) {
                if ($isVersioned && $versionedTable == $tableName) {
                    $this->_doUpdate($entity, $tableName, $data, $id);
                } else {
                    $this->_conn->update($tableName, $data, $id);
                }
            }
            if ($isVersioned && ! isset($updateData[$versionedTable])) {
                $this->_doUpdate($entity, $versionedTable, array(), $id);
            }
romanb's avatar
romanb committed
263 264
        }
    }
265

romanb's avatar
romanb committed
266 267 268 269 270 271 272 273 274
    /**
     * Deletes an entity.
     *
     * @param object $entity The entity to delete.
     * @override
     */
    public function delete($entity)
    {
        $id = array_combine(
275
            $this->_class->identifier,
276
            $this->_em->getUnitOfWork()->getEntityIdentifier($entity)
romanb's avatar
romanb committed
277
        );
278

romanb's avatar
romanb committed
279 280 281 282
        // If the database platform supports FKs, just
        // delete the row from the root table. Cascades do the rest.
        if ($this->_conn->getDatabasePlatform()->supportsForeignKeyConstraints()) {
            $this->_conn->delete($this->_em->getClassMetadata($this->_class->rootEntityName)
283
                    ->primaryTable['name'], $id);
romanb's avatar
romanb committed
284
        } else {
285
            // Delete from all tables individually, starting from this class' table up to the root table.
romanb's avatar
romanb committed
286 287 288 289 290 291
            $this->_conn->delete($this->_class->primaryTable['name'], $id);
            foreach ($this->_class->parentClasses as $parentClass) {
                $this->_conn->delete($this->_em->getClassMetadata($parentClass)->primaryTable['name'], $id);
            }
        }
    }
292

romanb's avatar
romanb committed
293
    /**
294
     * Gets the SELECT SQL to select one or more entities by a set of field criteria.
romanb's avatar
romanb committed
295 296 297 298 299
     *
     * @param array $criteria
     * @return string The SQL.
     * @override
     */
romanb's avatar
romanb committed
300
    protected function _getSelectEntitiesSql(array &$criteria, $assoc = null)
romanb's avatar
romanb committed
301 302 303 304 305
    {
        $tableAliases = array();
        $aliasIndex = 1;
        $idColumns = $this->_class->getIdentifierColumnNames();
        $baseTableAlias = 't0';
306
        $setResultColumnNames = empty($this->_resultColumnNames);
romanb's avatar
romanb committed
307 308 309 310
            
        foreach (array_merge($this->_class->subClasses, $this->_class->parentClasses) as $className) {
            $tableAliases[$className] = 't' . $aliasIndex++;
        }
311

312
        // Add regular columns
romanb's avatar
romanb committed
313 314 315
        $columnList = '';
        foreach ($this->_class->fieldMappings as $fieldName => $mapping) {
            $tableAlias = isset($mapping['inherited']) ?
romanb's avatar
romanb committed
316
                    $tableAliases[$mapping['inherited']] : $baseTableAlias;
romanb's avatar
romanb committed
317
            if ($columnList != '') $columnList .= ', ';
318
            $columnList .= $tableAlias . '.' . $this->_class->getQuotedColumnName($fieldName, $this->_platform);
319 320 321 322 323
          
            if ($setResultColumnNames) {
                $resultColumnName = $this->_platform->getSqlResultCasing($mapping['columnName']);
                $this->_resultColumnNames[$resultColumnName] = $mapping['columnName'];
            }
324 325
        }
        
326 327 328 329 330
        // Add foreign key columns
        foreach ($this->_class->associationMappings as $assoc2) {
            if ($assoc2->isOwningSide && $assoc2->isOneToOne()) {
                foreach ($assoc2->targetToSourceKeyColumns as $srcColumn) {
                    $columnList .= ', ' . $assoc2->getQuotedJoinColumnName($srcColumn, $this->_platform);
331 332 333 334 335
                    
                    if ($setResultColumnNames) {
                        $resultColumnName = $this->_platform->getSqlResultCasing($srcColumn);
                        $this->_resultColumnNames[$resultColumnName] = $srcColumn;
                    }
336 337 338 339
                }
            }
        }
        
340 341 342 343 344 345 346
        // Add discriminator column
        if ($this->_class->rootEntityName == $this->_class->name) {
            $columnList .= ', ' . $baseTableAlias . '.' .
                    $this->_class->getQuotedDiscriminatorColumnName($this->_platform);
        } else {
            $columnList .= ', ' . $tableAliases[$this->_class->rootEntityName] . '.' .
                    $this->_class->getQuotedDiscriminatorColumnName($this->_platform);
romanb's avatar
romanb committed
347
        }
348 349 350 351 352
        
        if ($setResultColumnNames) {
            $resultColumnName = $this->_platform->getSqlResultCasing($this->_class->discriminatorColumn['name']);
            $this->_resultColumnNames[$resultColumnName] = $this->_class->discriminatorColumn['name'];
        }
353

romanb's avatar
romanb committed
354
        // INNER JOIN parent tables
355
        $joinSql = '';
romanb's avatar
romanb committed
356 357 358
        foreach ($this->_class->parentClasses as $parentClassName) {
            $parentClass = $this->_em->getClassMetadata($parentClassName);
            $tableAlias = $tableAliases[$parentClassName];
359
            $joinSql .= ' INNER JOIN ' . $parentClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON ';
romanb's avatar
romanb committed
360 361
            $first = true;
            foreach ($idColumns as $idColumn) {
362 363
                if ($first) $first = false; else $joinSql .= ' AND ';
                $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
romanb's avatar
romanb committed
364 365
            }
        }
366

romanb's avatar
romanb committed
367 368 369 370
        // OUTER JOIN sub tables
        foreach ($this->_class->subClasses as $subClassName) {
            $subClass = $this->_em->getClassMetadata($subClassName);
            $tableAlias = $tableAliases[$subClassName];
371 372 373 374 375 376 377

            // Add subclass columns
            foreach ($subClass->fieldMappings as $fieldName => $mapping) {
                if (isset($mapping['inherited'])) {
                    continue;
                }
                $columnList .= ', ' . $tableAlias . '.' . $subClass->getQuotedColumnName($fieldName, $this->_platform);
378 379 380 381 382
                
                if ($setResultColumnNames) {
                    $resultColumnName = $this->_platform->getSqlResultCasing($mapping['columnName']);
                    $this->_resultColumnNames[$resultColumnName] = $mapping['columnName'];
                }
383 384 385 386 387 388 389
            }
            
            // Add join columns (foreign keys)
            foreach ($subClass->associationMappings as $assoc2) {
                if ($assoc2->isOwningSide && $assoc2->isOneToOne() && ! isset($subClass->inheritedAssociationFields[$assoc2->sourceFieldName])) {
                    foreach ($assoc2->targetToSourceKeyColumns as $srcColumn) {
                        $columnList .= ', ' . $tableAlias . '.' . $assoc2->getQuotedJoinColumnName($srcColumn, $this->_platform);
390 391 392 393 394
                        
                        if ($setResultColumnNames) {
                            $resultColumnName = $this->_platform->getSqlResultCasing($srcColumn);
                            $this->_resultColumnNames[$resultColumnName] = $srcColumn;
                        }
395 396 397 398 399 400
                    }
                }
            }
            
            // Add LEFT JOIN
            $joinSql .= ' LEFT JOIN ' . $subClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON ';
romanb's avatar
romanb committed
401 402
            $first = true;
            foreach ($idColumns as $idColumn) {
403 404
                if ($first) $first = false; else $joinSql .= ' AND ';
                $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
romanb's avatar
romanb committed
405 406
            }
        }
407

romanb's avatar
romanb committed
408 409 410
        $conditionSql = '';
        foreach ($criteria as $field => $value) {
            if ($conditionSql != '') $conditionSql .= ' AND ';
romanb's avatar
romanb committed
411 412 413 414 415 416
            $conditionSql .= $baseTableAlias . '.';
            if (isset($this->_class->columnNames[$field])) {
                $conditionSql .= $this->_class->getQuotedColumnName($field, $this->_platform);
            } else if ($assoc !== null) {
                $conditionSql .= $assoc->getQuotedJoinColumnName($field, $this->_platform);
            } else {
417
                throw ORMException::unrecognizedField($field);
romanb's avatar
romanb committed
418 419
            }
            $conditionSql .= ' = ?';
romanb's avatar
romanb committed
420
        }
421

422 423 424 425
        return 'SELECT ' . $columnList
                . ' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $baseTableAlias
                . $joinSql
                . ($conditionSql != '' ? ' WHERE ' . $conditionSql : '');
romanb's avatar
romanb committed
426
    }
427 428 429 430 431 432
    
    /** @override */
    protected function _processSqlResult(array $sqlResult)
    {
        return $this->_processSqlResultInheritanceAware($sqlResult);
    }
433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466
    
    /** @override */
    protected function _getInsertColumnList()
    {
        // Identifier columns must always come first in the column list of subclasses.
        $columns = $this->_class->parentClasses ? $this->_class->getIdentifierColumnNames() : array();

        foreach ($this->_class->reflFields as $name => $field) {
            if (isset($this->_class->fieldMappings[$name]['inherited']) && ! isset($this->_class->fieldMappings[$name]['id'])
                    || isset($this->_class->inheritedAssociationFields[$name])
                    || ($this->_class->isVersioned && $this->_class->versionField == $name)) {
                continue;
            }

            if (isset($this->_class->associationMappings[$name])) {
                $assoc = $this->_class->associationMappings[$name];
                if ($assoc->isOneToOne() && $assoc->isOwningSide) {
                    foreach ($assoc->targetToSourceKeyColumns as $sourceCol) {
                        $columns[] = $assoc->getQuotedJoinColumnName($sourceCol, $this->_platform);
                    }
                }
            } else if ($this->_class->name != $this->_class->rootEntityName ||
                    ! $this->_class->isIdGeneratorIdentity() || $this->_class->identifier[0] != $name) {
                $columns[] = $this->_class->getQuotedColumnName($name, $this->_platform);
            }
        }

        // Add discriminator column if it is the topmost class.
        if ($this->_class->name == $this->_class->rootEntityName) {
            $columns[] = $this->_class->getQuotedDiscriminatorColumnName($this->_platform);
        }

        return $columns;
    }
467
}