3 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15 * This software consists of voluntary contributions made by many individuals
16 * and is licensed under the MIT license. For more information, see
17 * <http://www.doctrine-project.org>.
20 namespace Doctrine\ORM\Persisters;
24 use Doctrine\DBAL\LockMode;
25 use Doctrine\DBAL\Types\Type;
26 use Doctrine\DBAL\Connection;
28 use Doctrine\ORM\ORMException;
29 use Doctrine\ORM\OptimisticLockException;
30 use Doctrine\ORM\EntityManager;
31 use Doctrine\ORM\UnitOfWork;
32 use Doctrine\ORM\Query;
33 use Doctrine\ORM\PersistentCollection;
34 use Doctrine\ORM\Mapping\MappingException;
35 use Doctrine\ORM\Mapping\ClassMetadata;
36 use Doctrine\ORM\Events;
37 use Doctrine\ORM\Event\LifecycleEventArgs;
39 use Doctrine\Common\Util\ClassUtils;
40 use Doctrine\Common\Collections\Criteria;
41 use Doctrine\Common\Collections\Expr\Comparison;
44 * A BasicEntityPersiter maps an entity to a single table in a relational database.
46 * A persister is always responsible for a single entity type.
48 * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent
49 * state of entities onto a relational database when the UnitOfWork is committed,
50 * as well as for basic querying of entities and their associations (not DQL).
52 * The persisting operations that are invoked during a commit of a UnitOfWork to
53 * persist the persistent entity state are:
55 * - {@link addInsert} : To schedule an entity for insertion.
56 * - {@link executeInserts} : To execute all scheduled insertions.
57 * - {@link update} : To update the persistent state of an entity.
58 * - {@link delete} : To delete the persistent state of an entity.
60 * As can be seen from the above list, insertions are batched and executed all at once
61 * for increased efficiency.
63 * The querying operations invoked during a UnitOfWork, either through direct find
64 * requests or lazy-loading, are the following:
66 * - {@link load} : Loads (the state of) a single, managed entity.
67 * - {@link loadAll} : Loads multiple, managed entities.
68 * - {@link loadOneToOneEntity} : Loads a one/many-to-one entity association (lazy-loading).
69 * - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading).
70 * - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading).
72 * The BasicEntityPersister implementation provides the default behavior for
73 * persisting and querying entities that are mapped to a single database table.
75 * Subclasses can be created to provide custom persisting and querying strategies,
76 * i.e. spanning multiple tables.
78 * @author Roman Borschel <roman@code-factory.org>
79 * @author Giorgio Sironi <piccoloprincipeazzurro@gmail.com>
80 * @author Benjamin Eberlei <kontakt@beberlei.de>
81 * @author Alexander <iam.asm89@gmail.com>
84 class BasicEntityPersister
89 static private $comparisonMap = array(
90 Comparison::EQ => '= %s',
91 Comparison::IS => '= %s',
92 Comparison::NEQ => '!= %s',
93 Comparison::GT => '> %s',
94 Comparison::GTE => '>= %s',
95 Comparison::LT => '< %s',
96 Comparison::LTE => '<= %s',
97 Comparison::IN => 'IN (%s)',
98 Comparison::NIN => 'NOT IN (%s)',
102 * Metadata object that describes the mapping of the mapped entity class.
104 * @var \Doctrine\ORM\Mapping\ClassMetadata
109 * The underlying DBAL Connection of the used EntityManager.
111 * @var \Doctrine\DBAL\Connection $conn
116 * The database platform.
118 * @var \Doctrine\DBAL\Platforms\AbstractPlatform
120 protected $_platform;
123 * The EntityManager instance.
125 * @var \Doctrine\ORM\EntityManager
134 protected $_queuedInserts = array();
137 * ResultSetMapping that is used for all queries. Is generated lazily once per request.
139 * TODO: Evaluate Caching in combination with the other cached SQL snippets.
141 * @var Query\ResultSetMapping
146 * The map of column names to DBAL mapping types of all prepared columns used
147 * when INSERTing or UPDATEing an entity.
150 * @see _prepareInsertData($entity)
151 * @see _prepareUpdateData($entity)
153 protected $_columnTypes = array();
156 * The map of quoted column names.
159 * @see _prepareInsertData($entity)
160 * @see _prepareUpdateData($entity)
162 protected $quotedColumns = array();
165 * The INSERT SQL statement used for entities handled by this persister.
166 * This SQL is only generated once per request, if at all.
173 * The SELECT column list SQL fragment used for querying entities by this persister.
174 * This SQL fragment is only generated once per request, if at all.
178 protected $_selectColumnListSql;
181 * The JOIN SQL fragement used to eagerly load all many-to-one and one-to-one
182 * associations configured as FETCH_EAGER, aswell as all inverse one-to-one associations.
186 protected $_selectJoinSql;
189 * Counter for creating unique SQL table and column aliases.
193 protected $_sqlAliasCounter = 0;
196 * Map from class names (FQCN) to the corresponding generated SQL table aliases.
200 protected $_sqlTableAliases = array();
203 * The quote strategy.
205 * @var \Doctrine\ORM\Mapping\QuoteStrategy
207 protected $quoteStrategy;
210 * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
211 * and persists instances of the class described by the given ClassMetadata descriptor.
213 * @param \Doctrine\ORM\EntityManager $em
214 * @param \Doctrine\ORM\Mapping\ClassMetadata $class
216 public function __construct(EntityManager $em, ClassMetadata $class)
219 $this->_class = $class;
220 $this->_conn = $em->getConnection();
221 $this->_platform = $this->_conn->getDatabasePlatform();
222 $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
226 * @return \Doctrine\ORM\Mapping\ClassMetadata
228 public function getClassMetadata()
230 return $this->_class;
234 * Adds an entity to the queued insertions.
235 * The entity remains queued until {@link executeInserts} is invoked.
237 * @param object $entity The entity to queue for insertion.
239 public function addInsert($entity)
241 $this->_queuedInserts[spl_object_hash($entity)] = $entity;
245 * Executes all queued entity insertions and returns any generated post-insert
246 * identifiers that were created as a result of the insertions.
248 * If no inserts are queued, invoking this method is a NOOP.
250 * @return array An array of any generated post-insert IDs. This will be an empty array
251 * if the entity class does not use the IDENTITY generation strategy.
253 public function executeInserts()
255 if ( ! $this->_queuedInserts) {
259 $postInsertIds = array();
260 $idGen = $this->_class->idGenerator;
261 $isPostInsertId = $idGen->isPostInsertGenerator();
263 $stmt = $this->_conn->prepare($this->_getInsertSQL());
264 $tableName = $this->_class->getTableName();
266 foreach ($this->_queuedInserts as $entity) {
267 $insertData = $this->_prepareInsertData($entity);
269 if (isset($insertData[$tableName])) {
272 foreach ($insertData[$tableName] as $column => $value) {
273 $stmt->bindValue($paramIndex++, $value, $this->_columnTypes[$column]);
279 if ($isPostInsertId) {
280 $id = $idGen->generate($this->_em, $entity);
281 $postInsertIds[$id] = $entity;
283 $id = $this->_class->getIdentifierValues($entity);
286 if ($this->_class->isVersioned) {
287 $this->assignDefaultVersionValue($entity, $id);
291 $stmt->closeCursor();
292 $this->_queuedInserts = array();
294 return $postInsertIds;
298 * Retrieves the default version value which was created
299 * by the preceding INSERT statement and assigns it back in to the
300 * entities version field.
302 * @param object $entity
305 protected function assignDefaultVersionValue($entity, $id)
307 $value = $this->fetchVersionValue($this->_class, $id);
308 $this->_class->setFieldValue($entity, $this->_class->versionField, $value);
312 * Fetch the current version value of a versioned entity.
314 * @param \Doctrine\ORM\Mapping\ClassMetadata $versionedClass
318 protected function fetchVersionValue($versionedClass, $id)
320 $versionField = $versionedClass->versionField;
321 $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->_platform);
323 $versionFieldColumnName = $this->quoteStrategy->getColumnName($versionField, $versionedClass, $this->_platform);
325 //FIXME: Order with composite keys might not be correct
326 $sql = 'SELECT ' . $versionFieldColumnName
327 . ' FROM ' . $this->quoteStrategy->getTableName($versionedClass, $this->_platform)
328 . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
329 $value = $this->_conn->fetchColumn($sql, array_values((array)$id));
331 return Type::getType($versionedClass->fieldMappings[$versionField]['type'])->convertToPHPValue($value, $this->_platform);
335 * Updates a managed entity. The entity is updated according to its current changeset
336 * in the running UnitOfWork. If there is no changeset, nothing is updated.
338 * The data to update is retrieved through {@link _prepareUpdateData}.
339 * Subclasses that override this method are supposed to obtain the update data
340 * in the same way, through {@link _prepareUpdateData}.
342 * Subclasses are also supposed to take care of versioning when overriding this method,
343 * if necessary. The {@link _updateTable} method can be used to apply the data retrieved
344 * from {@_prepareUpdateData} on the target tables, thereby optionally applying versioning.
346 * @param object $entity The entity to update.
348 public function update($entity)
350 $updateData = $this->_prepareUpdateData($entity);
351 $tableName = $this->_class->getTableName();
353 if (isset($updateData[$tableName]) && $updateData[$tableName]) {
355 $entity, $this->quoteStrategy->getTableName($this->_class, $this->_platform),
356 $updateData[$tableName], $this->_class->isVersioned
359 if ($this->_class->isVersioned) {
360 $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
361 $this->assignDefaultVersionValue($entity, $id);
367 * Performs an UPDATE statement for an entity on a specific table.
368 * The UPDATE can optionally be versioned, which requires the entity to have a version field.
370 * @param object $entity The entity object being updated.
371 * @param string $quotedTableName The quoted name of the table to apply the UPDATE on.
372 * @param array $updateData The map of columns to update (column => value).
373 * @param boolean $versioned Whether the UPDATE should be versioned.
375 protected final function _updateTable($entity, $quotedTableName, array $updateData, $versioned = false)
377 $set = $params = $types = array();
379 foreach ($updateData as $columnName => $value) {
380 $column = $columnName;
383 if (isset($this->_class->fieldNames[$columnName])) {
384 $column = $this->quoteStrategy->getColumnName($this->_class->fieldNames[$columnName], $this->_class, $this->_platform);
386 if (isset($this->_class->fieldMappings[$this->_class->fieldNames[$columnName]]['requireSQLConversion'])) {
387 $type = Type::getType($this->_columnTypes[$columnName]);
388 $placeholder = $type->convertToDatabaseValueSQL('?', $this->_platform);
390 } else if (isset($this->quotedColumns[$columnName])) {
391 $column = $this->quotedColumns[$columnName];
394 $set[] = $column . ' = ' . $placeholder;
396 $types[] = $this->_columnTypes[$columnName];
400 $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
402 foreach ($this->_class->identifier as $idField) {
403 if (isset($this->_class->associationMappings[$idField])) {
404 $targetMapping = $this->_em->getClassMetadata($this->_class->associationMappings[$idField]['targetEntity']);
405 $where[] = $this->_class->associationMappings[$idField]['joinColumns'][0]['name'];
406 $params[] = $id[$idField];
409 case (isset($targetMapping->fieldMappings[$targetMapping->identifier[0]])):
410 $types[] = $targetMapping->fieldMappings[$targetMapping->identifier[0]]['type'];
413 case (isset($targetMapping->associationMappings[$targetMapping->identifier[0]])):
414 $types[] = $targetMapping->associationMappings[$targetMapping->identifier[0]]['type'];
418 throw ORMException::unrecognizedField($targetMapping->identifier[0]);
421 $where[] = $this->quoteStrategy->getColumnName($idField, $this->_class, $this->_platform);
422 $params[] = $id[$idField];
423 $types[] = $this->_class->fieldMappings[$idField]['type'];
428 $versionField = $this->_class->versionField;
429 $versionFieldType = $this->_class->fieldMappings[$versionField]['type'];
430 $versionColumn = $this->quoteStrategy->getColumnName($versionField, $this->_class, $this->_platform);
432 if ($versionFieldType == Type::INTEGER) {
433 $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1';
434 } else if ($versionFieldType == Type::DATETIME) {
435 $set[] = $versionColumn . ' = CURRENT_TIMESTAMP';
438 $where[] = $versionColumn;
439 $params[] = $this->_class->reflFields[$versionField]->getValue($entity);
440 $types[] = $this->_class->fieldMappings[$versionField]['type'];
443 $sql = 'UPDATE ' . $quotedTableName
444 . ' SET ' . implode(', ', $set)
445 . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
447 $result = $this->_conn->executeUpdate($sql, $params, $types);
449 if ($versioned && ! $result) {
450 throw OptimisticLockException::lockFailed($entity);
455 * @todo Add check for platform if it supports foreign keys/cascading.
456 * @param array $identifier
459 protected function deleteJoinTableRecords($identifier)
461 foreach ($this->_class->associationMappings as $mapping) {
462 if ($mapping['type'] == ClassMetadata::MANY_TO_MANY) {
463 // @Todo this only covers scenarios with no inheritance or of the same level. Is there something
464 // like self-referential relationship between different levels of an inheritance hierachy? I hope not!
465 $selfReferential = ($mapping['targetEntity'] == $mapping['sourceEntity']);
466 $otherKeys = array();
469 if ( ! $mapping['isOwningSide']) {
470 $relatedClass = $this->_em->getClassMetadata($mapping['targetEntity']);
471 $mapping = $relatedClass->associationMappings[$mapping['mappedBy']];
473 foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
474 $keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $relatedClass, $this->_platform);
477 if ($selfReferential) {
478 foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
479 $otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $relatedClass, $this->_platform);
484 foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
485 $keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->_class, $this->_platform);
488 if ($selfReferential) {
489 foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
490 $otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->_class, $this->_platform);
495 if ( ! isset($mapping['isOnDeleteCascade'])) {
497 $joinTableName = $this->quoteStrategy->getJoinTableName($mapping, $this->_class, $this->_platform);
499 $this->_conn->delete($joinTableName, array_combine($keys, $identifier));
501 if ($selfReferential) {
502 $this->_conn->delete($joinTableName, array_combine($otherKeys, $identifier));
510 * Deletes a managed entity.
512 * The entity to delete must be managed and have a persistent identifier.
513 * The deletion happens instantaneously.
515 * Subclasses may override this method to customize the semantics of entity deletion.
517 * @param object $entity The entity to delete.
519 public function delete($entity)
521 $identifier = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
523 $this->deleteJoinTableRecords($identifier);
525 $id = array_combine($this->quoteStrategy->getIdentifierColumnNames($this->_class, $this->_platform), $identifier);
527 $this->_conn->delete($this->quoteStrategy->getTableName($this->_class, $this->_platform), $id);
531 * Prepares the changeset of an entity for database insertion (UPDATE).
533 * The changeset is obtained from the currently running UnitOfWork.
535 * During this preparation the array that is passed as the second parameter is filled with
536 * <columnName> => <value> pairs, grouped by table name.
541 * 'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...),
542 * 'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...),
547 * @param object $entity The entity for which to prepare the data.
548 * @return array The prepared data.
550 protected function _prepareUpdateData($entity)
553 $uow = $this->_em->getUnitOfWork();
555 if (($versioned = $this->_class->isVersioned) != false) {
556 $versionField = $this->_class->versionField;
559 foreach ($uow->getEntityChangeSet($entity) as $field => $change) {
560 if ($versioned && $versionField == $field) {
564 $newVal = $change[1];
566 if (isset($this->_class->associationMappings[$field])) {
567 $assoc = $this->_class->associationMappings[$field];
569 // Only owning side of x-1 associations can have a FK column.
570 if ( ! $assoc['isOwningSide'] || ! ($assoc['type'] & ClassMetadata::TO_ONE)) {
574 if ($newVal !== null) {
575 $oid = spl_object_hash($newVal);
577 if (isset($this->_queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) {
578 // The associated entity $newVal is not yet persisted, so we must
579 // set $newVal = null, in order to insert a null value and schedule an
580 // extra update on the UnitOfWork.
581 $uow->scheduleExtraUpdate($entity, array(
582 $field => array(null, $newVal)
588 if ($newVal !== null) {
589 $newValId = $uow->getEntityIdentifier($newVal);
592 $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']);
593 $owningTable = $this->getOwningTable($field);
595 foreach ($assoc['joinColumns'] as $joinColumn) {
596 $sourceColumn = $joinColumn['name'];
597 $targetColumn = $joinColumn['referencedColumnName'];
598 $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->_class, $this->_platform);
600 $this->quotedColumns[$sourceColumn] = $quotedColumn;
602 if ($newVal === null) {
603 $result[$owningTable][$sourceColumn] = null;
604 } else if ($targetClass->containsForeignIdentifier) {
605 $result[$owningTable][$sourceColumn] = $newValId[$targetClass->getFieldForColumn($targetColumn)];
607 $result[$owningTable][$sourceColumn] = $newValId[$targetClass->fieldNames[$targetColumn]];
610 $this->_columnTypes[$sourceColumn] = $targetClass->getTypeOfColumn($targetColumn);
613 $columnName = $this->_class->columnNames[$field];
614 $this->_columnTypes[$columnName] = $this->_class->fieldMappings[$field]['type'];
615 $result[$this->getOwningTable($field)][$columnName] = $newVal;
623 * Prepares the data changeset of a managed entity for database insertion (initial INSERT).
624 * The changeset of the entity is obtained from the currently running UnitOfWork.
626 * The default insert data preparation is the same as for updates.
628 * @param object $entity The entity for which to prepare the data.
629 * @return array The prepared data for the tables to update.
630 * @see _prepareUpdateData
632 protected function _prepareInsertData($entity)
634 return $this->_prepareUpdateData($entity);
638 * Gets the name of the table that owns the column the given field is mapped to.
640 * The default implementation in BasicEntityPersister always returns the name
641 * of the table the entity type of this persister is mapped to, since an entity
642 * is always persisted to a single table with a BasicEntityPersister.
644 * @param string $fieldName The field name.
645 * @return string The table name.
647 public function getOwningTable($fieldName)
649 return $this->_class->getTableName();
653 * Loads an entity by a list of field criteria.
655 * @param array $criteria The criteria by which to load the entity.
656 * @param object $entity The entity to load the data into. If not specified,
657 * a new entity is created.
658 * @param $assoc The association that connects the entity to load to another entity, if any.
659 * @param array $hints Hints for entity creation.
660 * @param int $lockMode
661 * @param int $limit Limit number of results
662 * @return object The loaded and managed entity instance or NULL if the entity can not be found.
663 * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
665 public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0, $limit = null)
667 $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, $lockMode, $limit);
668 list($params, $types) = $this->expandParameters($criteria);
669 $stmt = $this->_conn->executeQuery($sql, $params, $types);
671 if ($entity !== null) {
672 $hints[Query::HINT_REFRESH] = true;
673 $hints[Query::HINT_REFRESH_ENTITY] = $entity;
676 $hydrator = $this->_em->newHydrator($this->_selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
677 $entities = $hydrator->hydrateAll($stmt, $this->_rsm, $hints);
679 return $entities ? $entities[0] : null;
683 * Loads an entity of this persister's mapped class as part of a single-valued
684 * association from another entity.
686 * @param array $assoc The association to load.
687 * @param object $sourceEntity The entity that owns the association (not necessarily the "owning side").
688 * @param array $identifier The identifier of the entity to load. Must be provided if
689 * the association to load represents the owning side, otherwise
690 * the identifier is derived from the $sourceEntity.
691 * @return object The loaded and managed entity instance or NULL if the entity can not be found.
693 public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = array())
695 if (($foundEntity = $this->_em->getUnitOfWork()->tryGetById($identifier, $assoc['targetEntity'])) != false) {
699 $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']);
701 if ($assoc['isOwningSide']) {
702 $isInverseSingleValued = $assoc['inversedBy'] && ! $targetClass->isCollectionValuedAssociation($assoc['inversedBy']);
704 // Mark inverse side as fetched in the hints, otherwise the UoW would
705 // try to load it in a separate query (remember: to-one inverse sides can not be lazy).
708 if ($isInverseSingleValued) {
709 $hints['fetched']["r"][$assoc['inversedBy']] = true;
712 /* cascade read-only status
713 if ($this->_em->getUnitOfWork()->isReadOnly($sourceEntity)) {
714 $hints[Query::HINT_READ_ONLY] = true;
718 $targetEntity = $this->load($identifier, null, $assoc, $hints);
720 // Complete bidirectional association, if necessary
721 if ($targetEntity !== null && $isInverseSingleValued) {
722 $targetClass->reflFields[$assoc['inversedBy']]->setValue($targetEntity, $sourceEntity);
725 $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']);
726 $owningAssoc = $targetClass->getAssociationMapping($assoc['mappedBy']);
728 // TRICKY: since the association is specular source and target are flipped
729 foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
730 if ( ! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
731 throw MappingException::joinColumnMustPointToMappedField(
732 $sourceClass->name, $sourceKeyColumn
736 // unset the old value and set the new sql aliased value here. By definition
737 // unset($identifier[$targetKeyColumn] works here with how UnitOfWork::createEntity() calls this method.
738 $identifier[$this->_getSQLTableAlias($targetClass->name) . "." . $targetKeyColumn] =
739 $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
741 unset($identifier[$targetKeyColumn]);
744 $targetEntity = $this->load($identifier, null, $assoc);
746 if ($targetEntity !== null) {
747 $targetClass->setFieldValue($targetEntity, $assoc['mappedBy'], $sourceEntity);
751 return $targetEntity;
755 * Refreshes a managed entity.
757 * @param array $id The identifier of the entity as an associative array from
758 * column or field names to values.
759 * @param object $entity The entity to refresh.
761 public function refresh(array $id, $entity, $lockMode = 0)
763 $sql = $this->_getSelectEntitiesSQL($id, null, $lockMode);
764 list($params, $types) = $this->expandParameters($id);
765 $stmt = $this->_conn->executeQuery($sql, $params, $types);
767 $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT);
768 $hydrator->hydrateAll($stmt, $this->_rsm, array(Query::HINT_REFRESH => true));
772 * Load Entities matching the given Criteria object
774 * @param \Doctrine\Common\Collections\Criteria $criteria
778 public function loadCriteria(Criteria $criteria)
780 $orderBy = $criteria->getOrderings();
781 $limit = $criteria->getMaxResults();
782 $offset = $criteria->getFirstResult();
784 $sql = $this->_getSelectEntitiesSQL($criteria, null, 0, $limit, $offset, $orderBy);
786 list($params, $types) = $this->expandCriteriaParameters($criteria);
788 $stmt = $this->_conn->executeQuery($sql, $params, $types);
790 $hydrator = $this->_em->newHydrator(($this->_selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
792 return $hydrator->hydrateAll($stmt, $this->_rsm, array('deferEagerLoads' => true));
796 * Expand Criteria Parameters by walking the expressions and grabbing all
797 * parameters and types from it.
799 * @param \Doctrine\Common\Collections\Criteria $criteria
801 * @return array(array(), array())
803 private function expandCriteriaParameters(Criteria $criteria)
805 $expression = $criteria->getWhereExpression();
807 if ($expression === null) {
808 return array(array(), array());
811 $valueVisitor = new SqlValueVisitor();
812 $valueVisitor->dispatch($expression);
814 list($values, $types) = $valueVisitor->getParamsAndTypes();
816 $sqlValues = array();
817 foreach ($values as $value) {
818 $sqlValues[] = $this->getValue($value);
822 foreach ($types as $type) {
823 list($field, $value) = $type;
824 $sqlTypes[] = $this->getType($field, $value);
827 return array($sqlValues, $sqlTypes);
831 * Loads a list of entities by a list of field criteria.
833 * @param array $criteria
834 * @param array $orderBy
839 public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null)
841 $sql = $this->_getSelectEntitiesSQL($criteria, null, 0, $limit, $offset, $orderBy);
842 list($params, $types) = $this->expandParameters($criteria);
843 $stmt = $this->_conn->executeQuery($sql, $params, $types);
845 $hydrator = $this->_em->newHydrator(($this->_selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
847 return $hydrator->hydrateAll($stmt, $this->_rsm, array('deferEagerLoads' => true));
851 * Get (sliced or full) elements of the given collection.
853 * @param array $assoc
854 * @param object $sourceEntity
855 * @param int|null $offset
856 * @param int|null $limit
859 public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
861 $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit);
863 return $this->loadArrayFromStatement($assoc, $stmt);
867 * Load an array of entities from a given dbal statement.
869 * @param array $assoc
870 * @param \Doctrine\DBAL\Statement $stmt
874 private function loadArrayFromStatement($assoc, $stmt)
876 $hints = array('deferEagerLoads' => true);
878 if (isset($assoc['indexBy'])) {
879 $rsm = clone ($this->_rsm); // this is necessary because the "default rsm" should be changed.
880 $rsm->addIndexBy('r', $assoc['indexBy']);
885 $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT);
887 return $hydrator->hydrateAll($stmt, $rsm, $hints);
891 * Hydrate a collection from a given dbal statement.
893 * @param array $assoc
894 * @param \Doctrine\DBAL\Statement $stmt
895 * @param PersistentCollection $coll
899 private function loadCollectionFromStatement($assoc, $stmt, $coll)
901 $hints = array('deferEagerLoads' => true, 'collection' => $coll);
903 if (isset($assoc['indexBy'])) {
904 $rsm = clone ($this->_rsm); // this is necessary because the "default rsm" should be changed.
905 $rsm->addIndexBy('r', $assoc['indexBy']);
910 $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT);
912 return $hydrator->hydrateAll($stmt, $rsm, $hints);
916 * Loads a collection of entities of a many-to-many association.
918 * @param ManyToManyMapping $assoc The association mapping of the association being loaded.
919 * @param object $sourceEntity The entity that owns the collection.
920 * @param PersistentCollection $coll The collection to fill.
921 * @param int|null $offset
922 * @param int|null $limit
925 public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
927 $stmt = $this->getManyToManyStatement($assoc, $sourceEntity);
929 return $this->loadCollectionFromStatement($assoc, $stmt, $coll);
932 private function getManyToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
935 $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']);
937 if ($assoc['isOwningSide']) {
938 $quotedJoinTable = $this->quoteStrategy->getJoinTableName($assoc, $sourceClass, $this->_platform);
940 foreach ($assoc['joinTable']['joinColumns'] as $joinColumn) {
941 $relationKeyColumn = $joinColumn['name'];
942 $sourceKeyColumn = $joinColumn['referencedColumnName'];
943 $quotedKeyColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $sourceClass, $this->_platform);
945 if ($sourceClass->containsForeignIdentifier) {
946 $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
947 $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
949 if (isset($sourceClass->associationMappings[$field])) {
950 $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value);
951 $value = $value[$this->_em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
954 $criteria[$quotedJoinTable . "." . $quotedKeyColumn] = $value;
955 } else if (isset($sourceClass->fieldNames[$sourceKeyColumn])) {
956 $criteria[$quotedJoinTable . "." . $quotedKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
958 throw MappingException::joinColumnMustPointToMappedField(
959 $sourceClass->name, $sourceKeyColumn
964 $owningAssoc = $this->_em->getClassMetadata($assoc['targetEntity'])->associationMappings[$assoc['mappedBy']];
965 $quotedJoinTable = $this->quoteStrategy->getJoinTableName($owningAssoc, $sourceClass, $this->_platform);
967 // TRICKY: since the association is inverted source and target are flipped
968 foreach ($owningAssoc['joinTable']['inverseJoinColumns'] as $joinColumn) {
969 $relationKeyColumn = $joinColumn['name'];
970 $sourceKeyColumn = $joinColumn['referencedColumnName'];
971 $quotedKeyColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $sourceClass, $this->_platform);
973 if ($sourceClass->containsForeignIdentifier) {
974 $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
975 $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
977 if (isset($sourceClass->associationMappings[$field])) {
978 $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value);
979 $value = $value[$this->_em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
982 $criteria[$quotedJoinTable . "." . $quotedKeyColumn] = $value;
983 } else if (isset($sourceClass->fieldNames[$sourceKeyColumn])) {
984 $criteria[$quotedJoinTable . "." . $quotedKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
986 throw MappingException::joinColumnMustPointToMappedField(
987 $sourceClass->name, $sourceKeyColumn
993 $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset);
994 list($params, $types) = $this->expandParameters($criteria);
996 return $this->_conn->executeQuery($sql, $params, $types);
1000 * Gets the SELECT SQL to select one or more entities by a set of field criteria.
1002 * @param array|\Doctrine\Common\Collections\Criteria $criteria
1003 * @param AssociationMapping $assoc
1004 * @param string $orderBy
1005 * @param int $lockMode
1007 * @param int $offset
1008 * @param array $orderBy
1010 * @todo Refactor: _getSelectSQL(...)
1012 protected function _getSelectEntitiesSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null)
1014 $joinSql = ($assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY) ? $this->_getSelectManyToManyJoinSQL($assoc) : '';
1015 $conditionSql = ($criteria instanceof Criteria)
1016 ? $this->_getSelectConditionCriteriaSQL($criteria)
1017 : $this->_getSelectConditionSQL($criteria, $assoc);
1019 $orderBy = ($assoc !== null && isset($assoc['orderBy'])) ? $assoc['orderBy'] : $orderBy;
1020 $orderBySql = $orderBy ? $this->_getOrderBySQL($orderBy, $this->_getSQLTableAlias($this->_class->name)) : '';
1024 if ($lockMode == LockMode::PESSIMISTIC_READ) {
1025 $lockSql = ' ' . $this->_platform->getReadLockSql();
1026 } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) {
1027 $lockSql = ' ' . $this->_platform->getWriteLockSql();
1030 $alias = $this->_getSQLTableAlias($this->_class->name);
1032 if ($filterSql = $this->generateFilterConditionSQL($this->_class, $alias)) {
1033 if ($conditionSql) {
1034 $conditionSql .= ' AND ';
1037 $conditionSql .= $filterSql;
1040 return $this->_platform->modifyLimitQuery('SELECT ' . $this->_getSelectColumnListSQL()
1041 . $this->_platform->appendLockHint(' FROM ' . $this->quoteStrategy->getTableName($this->_class, $this->_platform) . ' '
1042 . $alias, $lockMode)
1043 . $this->_selectJoinSql . $joinSql
1044 . ($conditionSql ? ' WHERE ' . $conditionSql : '')
1045 . $orderBySql, $limit, $offset)
1050 * Gets the ORDER BY SQL snippet for ordered collections.
1052 * @param array $orderBy
1053 * @param string $baseTableAlias
1056 protected final function _getOrderBySQL(array $orderBy, $baseTableAlias)
1060 foreach ($orderBy as $fieldName => $orientation) {
1061 if ( ! isset($this->_class->fieldMappings[$fieldName])) {
1062 throw ORMException::unrecognizedField($fieldName);
1065 $orientation = strtoupper(trim($orientation));
1066 if ($orientation != 'ASC' && $orientation != 'DESC') {
1067 throw ORMException::invalidOrientation($this->_class->name, $fieldName);
1070 $tableAlias = isset($this->_class->fieldMappings[$fieldName]['inherited']) ?
1071 $this->_getSQLTableAlias($this->_class->fieldMappings[$fieldName]['inherited'])
1074 $columnName = $this->quoteStrategy->getColumnName($fieldName, $this->_class, $this->_platform);
1076 $orderBySql .= $orderBySql ? ', ' : ' ORDER BY ';
1077 $orderBySql .= $tableAlias . '.' . $columnName . ' ' . $orientation;
1084 * Gets the SQL fragment with the list of columns to select when querying for
1085 * an entity in this persister.
1087 * Subclasses should override this method to alter or change the select column
1088 * list SQL fragment. Note that in the implementation of BasicEntityPersister
1089 * the resulting SQL fragment is generated only once and cached in {@link _selectColumnListSql}.
1090 * Subclasses may or may not do the same.
1092 * @return string The SQL fragment.
1093 * @todo Rename: _getSelectColumnsSQL()
1095 protected function _getSelectColumnListSQL()
1097 if ($this->_selectColumnListSql !== null) {
1098 return $this->_selectColumnListSql;
1102 $this->_rsm = new Query\ResultSetMapping();
1103 $this->_rsm->addEntityResult($this->_class->name, 'r'); // r for root
1105 // Add regular columns to select list
1106 foreach ($this->_class->fieldNames as $field) {
1107 if ($columnList) $columnList .= ', ';
1109 $columnList .= $this->_getSelectColumnSQL($field, $this->_class);
1112 $this->_selectJoinSql = '';
1113 $eagerAliasCounter = 0;
1115 foreach ($this->_class->associationMappings as $assocField => $assoc) {
1116 $assocColumnSQL = $this->_getSelectColumnAssociationSQL($assocField, $assoc, $this->_class);
1118 if ($assocColumnSQL) {
1119 if ($columnList) $columnList .= ', ';
1121 $columnList .= $assocColumnSQL;
1124 if ($assoc['type'] & ClassMetadata::TO_ONE && ($assoc['fetch'] == ClassMetadata::FETCH_EAGER || !$assoc['isOwningSide'])) {
1125 $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']);
1127 if ($eagerEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) {
1128 continue; // now this is why you shouldn't use inheritance
1131 $assocAlias = 'e' . ($eagerAliasCounter++);
1132 $this->_rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField);
1134 foreach ($eagerEntity->fieldNames as $field) {
1135 if ($columnList) $columnList .= ', ';
1137 $columnList .= $this->_getSelectColumnSQL($field, $eagerEntity, $assocAlias);
1140 foreach ($eagerEntity->associationMappings as $assoc2Field => $assoc2) {
1141 $assoc2ColumnSQL = $this->_getSelectColumnAssociationSQL($assoc2Field, $assoc2, $eagerEntity, $assocAlias);
1143 if ($assoc2ColumnSQL) {
1144 if ($columnList) $columnList .= ', ';
1145 $columnList .= $assoc2ColumnSQL;
1150 if ($assoc['isOwningSide']) {
1151 $this->_selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($assoc['joinColumns']);
1152 $this->_selectJoinSql .= ' ' . $this->quoteStrategy->getTableName($eagerEntity, $this->_platform) . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON ';
1154 $tableAlias = $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias);
1155 foreach ($assoc['joinColumns'] as $joinColumn) {
1156 $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->_class, $this->_platform);
1157 $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->_class, $this->_platform);
1160 $this->_selectJoinSql .= ' AND ';
1162 $this->_selectJoinSql .= $this->_getSQLTableAlias($assoc['sourceEntity']) . '.' . $sourceCol . ' = '
1163 . $tableAlias . '.' . $targetCol;
1168 if ($filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias)) {
1169 $this->_selectJoinSql .= ' AND ' . $filterSql;
1172 $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']);
1173 $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']);
1175 $this->_selectJoinSql .= ' LEFT JOIN';
1176 $this->_selectJoinSql .= ' ' . $this->quoteStrategy->getTableName($eagerEntity, $this->_platform) . ' '
1177 . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) . ' ON ';
1179 foreach ($owningAssoc['sourceToTargetKeyColumns'] as $sourceCol => $targetCol) {
1181 $this->_selectJoinSql .= ' AND ';
1184 $this->_selectJoinSql .= $this->_getSQLTableAlias($owningAssoc['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = '
1185 . $this->_getSQLTableAlias($owningAssoc['targetEntity']) . '.' . $targetCol;
1192 $this->_selectColumnListSql = $columnList;
1194 return $this->_selectColumnListSql;
1198 * Gets the SQL join fragment used when selecting entities from an association.
1200 * @param string $field
1201 * @param array $assoc
1202 * @param ClassMetadata $class
1203 * @param string $alias
1207 protected function _getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r')
1209 $columnList = array();
1211 if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
1213 foreach ($assoc['joinColumns'] as $joinColumn) {
1215 $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->_class, $this->_platform);
1216 $resultColumnName = $this->getSQLColumnAlias($joinColumn['name']);
1217 $columnList[] = $this->_getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) )
1218 . '.' . $quotedColumn . ' AS ' . $resultColumnName;
1220 $this->_rsm->addMetaResult($alias, $resultColumnName, $quotedColumn, isset($assoc['id']) && $assoc['id'] === true);
1224 return implode(', ', $columnList);
1228 * Gets the SQL join fragment used when selecting entities from a
1229 * many-to-many association.
1231 * @param ManyToManyMapping $manyToMany
1234 protected function _getSelectManyToManyJoinSQL(array $manyToMany)
1236 $conditions = array();
1237 $association = $manyToMany;
1238 $sourceTableAlias = $this->_getSQLTableAlias($this->_class->name);
1240 if ( ! $manyToMany['isOwningSide']) {
1241 $targetEntity = $this->_em->getClassMetadata($manyToMany['targetEntity']);
1242 $association = $targetEntity->associationMappings[$manyToMany['mappedBy']];
1245 $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->_class, $this->_platform);
1246 $joinColumns = ($manyToMany['isOwningSide'])
1247 ? $association['joinTable']['inverseJoinColumns']
1248 : $association['joinTable']['joinColumns'];
1250 foreach ($joinColumns as $joinColumn) {
1251 $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->_class, $this->_platform);
1252 $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->_class, $this->_platform);
1253 $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableName . '.' . $quotedSourceColumn;
1256 return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
1260 * Gets the INSERT SQL used by the persister to persist a new entity.
1264 protected function _getInsertSQL()
1266 if ($this->_insertSql === null) {
1268 $columns = $this->_getInsertColumnList();
1270 if (empty($columns)) {
1271 $insertSql = $this->_platform->getEmptyIdentityInsertSQL(
1272 $this->quoteStrategy->getTableName($this->_class, $this->_platform),
1273 $this->quoteStrategy->getColumnName($this->_class->identifier[0], $this->_class, $this->_platform)
1276 $columns = array_unique($columns);
1279 foreach ($columns as $column) {
1282 if (isset($this->_class->fieldNames[$column]) &&
1283 isset($this->_columnTypes[$this->_class->fieldNames[$column]]) &&
1284 isset($this->_class->fieldMappings[$this->_class->fieldNames[$column]]['requireSQLConversion'])) {
1285 $type = Type::getType($this->_columnTypes[$this->_class->fieldNames[$column]]);
1286 $placeholder = $type->convertToDatabaseValueSQL('?', $this->_platform);
1289 $values[] = $placeholder;
1292 $insertSql = 'INSERT INTO ' . $this->quoteStrategy->getTableName($this->_class, $this->_platform)
1293 . ' (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $values) . ')';
1296 $this->_insertSql = $insertSql;
1299 return $this->_insertSql;
1303 * Gets the list of columns to put in the INSERT SQL statement.
1305 * Subclasses should override this method to alter or change the list of
1306 * columns placed in the INSERT statements used by the persister.
1308 * @return array The list of columns.
1310 protected function _getInsertColumnList()
1314 foreach ($this->_class->reflFields as $name => $field) {
1315 if ($this->_class->isVersioned && $this->_class->versionField == $name) {
1319 if (isset($this->_class->associationMappings[$name])) {
1320 $assoc = $this->_class->associationMappings[$name];
1321 if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
1322 foreach ($assoc['joinColumns'] as $joinColumn) {
1323 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->_class, $this->_platform);
1326 } else if ($this->_class->generatorType != ClassMetadata::GENERATOR_TYPE_IDENTITY || $this->_class->identifier[0] != $name) {
1327 $columns[] = $this->quoteStrategy->getColumnName($name, $this->_class, $this->_platform);
1328 $this->_columnTypes[$name] = $this->_class->fieldMappings[$name]['type'];
1336 * Gets the SQL snippet of a qualified column name for the given field name.
1338 * @param string $field The field name.
1339 * @param ClassMetadata $class The class that declares this field. The table this class is
1340 * mapped to must own the column for the given field.
1341 * @param string $alias
1343 protected function _getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
1345 $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias)
1346 . '.' . $this->quoteStrategy->getColumnName($field, $class, $this->_platform);
1347 $columnAlias = $this->getSQLColumnAlias($class->columnNames[$field]);
1349 $this->_rsm->addFieldResult($alias, $columnAlias, $field);
1351 if (isset($class->fieldMappings[$field]['requireSQLConversion'])) {
1352 $type = Type::getType($class->getTypeOfField($field));
1353 $sql = $type->convertToPHPValueSQL($sql, $this->_platform);
1356 return $sql . ' AS ' . $columnAlias;
1360 * Gets the SQL table alias for the given class name.
1362 * @param string $className
1363 * @return string The SQL table alias.
1364 * @todo Reconsider. Binding table aliases to class names is not such a good idea.
1366 protected function _getSQLTableAlias($className, $assocName = '')
1369 $className .= '#' . $assocName;
1372 if (isset($this->_sqlTableAliases[$className])) {
1373 return $this->_sqlTableAliases[$className];
1376 $tableAlias = 't' . $this->_sqlAliasCounter++;
1378 $this->_sqlTableAliases[$className] = $tableAlias;
1384 * Lock all rows of this entity matching the given criteria with the specified pessimistic lock mode
1386 * @param array $criteria
1387 * @param int $lockMode
1390 public function lock(array $criteria, $lockMode)
1392 $conditionSql = $this->_getSelectConditionSQL($criteria);
1394 if ($lockMode == LockMode::PESSIMISTIC_READ) {
1395 $lockSql = $this->_platform->getReadLockSql();
1396 } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) {
1397 $lockSql = $this->_platform->getWriteLockSql();
1401 . $this->_platform->appendLockHint($this->getLockTablesSql(), $lockMode)
1402 . ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ' . $lockSql;
1404 list($params, $types) = $this->expandParameters($criteria);
1406 $this->_conn->executeQuery($sql, $params, $types);
1410 * Get the FROM and optionally JOIN conditions to lock the entity managed by this persister.
1414 protected function getLockTablesSql()
1416 return 'FROM ' . $this->quoteStrategy->getTableName($this->_class, $this->_platform) . ' '
1417 . $this->_getSQLTableAlias($this->_class->name);
1421 * Get the Select Where Condition from a Criteria object.
1423 * @param \Doctrine\Common\Collections\Criteria $criteria
1426 protected function _getSelectConditionCriteriaSQL(Criteria $criteria)
1428 $expression = $criteria->getWhereExpression();
1430 if ($expression === null) {
1434 $visitor = new SqlExpressionVisitor($this);
1436 return $visitor->dispatch($expression);
1440 * Get the SQL WHERE condition for matching a field with a given value.
1442 * @param string $field
1443 * @param mixed $value
1444 * @param array|null $assoc
1445 * @param string $comparison
1449 public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null)
1451 $conditionSql = $this->getSelectConditionStatementColumnSQL($field, $assoc);
1454 if (isset($this->_class->fieldMappings[$field]['requireSQLConversion'])) {
1455 $type = Type::getType($this->_class->getTypeOfField($field));
1456 $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->_platform);
1459 $conditionSql .= ($comparison === null)
1460 ? ((is_array($value)) ? ' IN (?)' : (($value === null) ? ' IS NULL' : ' = ' . $placeholder))
1461 : ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
1464 return $conditionSql;
1468 * Build the left-hand-side of a where condition statement.
1470 * @param string $field
1471 * @param array $assoc
1475 protected function getSelectConditionStatementColumnSQL($field, $assoc = null)
1478 case (isset($this->_class->columnNames[$field])):
1479 $className = (isset($this->_class->fieldMappings[$field]['inherited']))
1480 ? $this->_class->fieldMappings[$field]['inherited']
1481 : $this->_class->name;
1483 return $this->_getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->_class, $this->_platform);
1485 case (isset($this->_class->associationMappings[$field])):
1486 if ( ! $this->_class->associationMappings[$field]['isOwningSide']) {
1487 throw ORMException::invalidFindByInverseAssociation($this->_class->name, $field);
1490 $className = (isset($this->_class->associationMappings[$field]['inherited']))
1491 ? $this->_class->associationMappings[$field]['inherited']
1492 : $this->_class->name;
1494 return $this->_getSQLTableAlias($className) . '.' . $this->_class->associationMappings[$field]['joinColumns'][0]['name'];
1496 case ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false):
1497 // very careless developers could potentially open up this normally hidden api for userland attacks,
1498 // therefore checking for spaces and function calls which are not allowed.
1500 // found a join column condition, not really a "field"
1504 throw ORMException::unrecognizedField($field);
1508 * Gets the conditional SQL fragment used in the WHERE clause when selecting
1509 * entities in this persister.
1511 * Subclasses are supposed to override this method if they intend to change
1512 * or alter the criteria by which entities are selected.
1514 * @param array $criteria
1515 * @param AssociationMapping $assoc
1518 protected function _getSelectConditionSQL(array $criteria, $assoc = null)
1522 foreach ($criteria as $field => $value) {
1523 $conditionSql .= $conditionSql ? ' AND ' : '';
1524 $conditionSql .= $this->getSelectConditionStatementSQL($field, $value, $assoc);
1527 return $conditionSql;
1531 * Return an array with (sliced or full list) of elements in the specified collection.
1533 * @param array $assoc
1534 * @param object $sourceEntity
1535 * @param int $offset
1539 public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
1541 $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit);
1543 return $this->loadArrayFromStatement($assoc, $stmt);
1547 * Loads a collection of entities in a one-to-many association.
1549 * @param array $assoc
1550 * @param object $sourceEntity
1551 * @param PersistentCollection $coll The collection to load/fill.
1552 * @param int|null $offset
1553 * @param int|null $limit
1555 public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
1557 $stmt = $this->getOneToManyStatement($assoc, $sourceEntity);
1559 return $this->loadCollectionFromStatement($assoc, $stmt, $coll);
1563 * Build criteria and execute SQL statement to fetch the one to many entities from.
1565 * @param array $assoc
1566 * @param object $sourceEntity
1567 * @param int|null $offset
1568 * @param int|null $limit
1569 * @return \Doctrine\DBAL\Statement
1571 private function getOneToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
1573 $criteria = array();
1574 $owningAssoc = $this->_class->associationMappings[$assoc['mappedBy']];
1575 $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']);
1577 $tableAlias = $this->_getSQLTableAlias(isset($owningAssoc['inherited']) ? $owningAssoc['inherited'] : $this->_class->name);
1579 foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
1580 if ($sourceClass->containsForeignIdentifier) {
1581 $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
1582 $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1584 if (isset($sourceClass->associationMappings[$field])) {
1585 $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value);
1586 $value = $value[$this->_em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
1589 $criteria[$tableAlias . "." . $targetKeyColumn] = $value;
1591 $criteria[$tableAlias . "." . $targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
1595 $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset);
1596 list($params, $types) = $this->expandParameters($criteria);
1598 return $this->_conn->executeQuery($sql, $params, $types);
1602 * Expand the parameters from the given criteria and use the correct binding types if found.
1604 * @param array $criteria
1607 private function expandParameters($criteria)
1609 $params = $types = array();
1611 foreach ($criteria as $field => $value) {
1612 if ($value === null) {
1613 continue; // skip null values.
1616 $types[] = $this->getType($field, $value);
1617 $params[] = $this->getValue($value);
1620 return array($params, $types);
1624 * Infer field type to be used by parameter type casting.
1626 * @param string $field
1627 * @param mixed $value
1630 private function getType($field, $value)
1633 case (isset($this->_class->fieldMappings[$field])):
1634 $type = $this->_class->fieldMappings[$field]['type'];
1637 case (isset($this->_class->associationMappings[$field])):
1638 $assoc = $this->_class->associationMappings[$field];
1640 if (count($assoc['sourceToTargetKeyColumns']) > 1) {
1641 throw Query\QueryException::associationPathCompositeKeyNotSupported();
1644 $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']);
1645 $targetColumn = $assoc['joinColumns'][0]['referencedColumnName'];
1648 if (isset($targetClass->fieldNames[$targetColumn])) {
1649 $type = $targetClass->fieldMappings[$targetClass->fieldNames[$targetColumn]]['type'];
1657 if (is_array($value)) {
1658 $type = Type::getType( $type )->getBindingType();
1659 $type += Connection::ARRAY_PARAM_OFFSET;
1666 * Retrieve parameter value
1668 * @param mixed $value
1671 private function getValue($value)
1673 if (is_array($value)) {
1674 $newValue = array();
1676 foreach ($value as $itemValue) {
1677 $newValue[] = $this->getIndividualValue($itemValue);
1683 return $this->getIndividualValue($value);
1687 * Retrieve an invidiual parameter value
1689 * @param mixed $value
1692 private function getIndividualValue($value)
1694 if (is_object($value) && $this->_em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($value))) {
1695 if ($this->_em->getUnitOfWork()->getEntityState($value) === UnitOfWork::STATE_MANAGED) {
1696 $idValues = $this->_em->getUnitOfWork()->getEntityIdentifier($value);
1698 $class = $this->_em->getClassMetadata(get_class($value));
1699 $idValues = $class->getIdentifierValues($value);
1702 $value = $idValues[key($idValues)];
1709 * Checks whether the given managed entity exists in the database.
1711 * @param object $entity
1712 * @return boolean TRUE if the entity exists in the database, FALSE otherwise.
1714 public function exists($entity, array $extraConditions = array())
1716 $criteria = $this->_class->getIdentifierValues($entity);
1722 if ($extraConditions) {
1723 $criteria = array_merge($criteria, $extraConditions);
1726 $alias = $this->_getSQLTableAlias($this->_class->name);
1729 . $this->getLockTablesSql()
1730 . ' WHERE ' . $this->_getSelectConditionSQL($criteria);
1732 if ($filterSql = $this->generateFilterConditionSQL($this->_class, $alias)) {
1733 $sql .= ' AND ' . $filterSql;
1736 list($params) = $this->expandParameters($criteria);
1738 return (bool) $this->_conn->fetchColumn($sql, $params);
1742 * Generates the appropriate join SQL for the given join column.
1744 * @param array $joinColumns The join columns definition of an association.
1745 * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise.
1747 protected function getJoinSQLForJoinColumns($joinColumns)
1749 // if one of the join columns is nullable, return left join
1750 foreach ($joinColumns as $joinColumn) {
1751 if ( ! isset($joinColumn['nullable']) || $joinColumn['nullable']) {
1756 return 'INNER JOIN';
1760 * Gets an SQL column alias for a column name.
1762 * @param string $columnName
1765 public function getSQLColumnAlias($columnName)
1767 return $this->quoteStrategy->getColumnAlias($columnName, $this->_sqlAliasCounter++, $this->_platform);
1771 * Generates the filter SQL for a given entity and table alias.
1773 * @param ClassMetadata $targetEntity Metadata of the target entity.
1774 * @param string $targetTableAlias The table alias of the joined/selected table.
1776 * @return string The SQL query part to add to a query.
1778 protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
1780 $filterClauses = array();
1782 foreach ($this->_em->getFilters()->getEnabledFilters() as $filter) {
1783 if ('' !== $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
1784 $filterClauses[] = '(' . $filterExpr . ')';
1788 $sql = implode(' AND ', $filterClauses);
1789 return $sql ? "(" . $sql . ")" : ""; // Wrap again to avoid "X or Y and FilterConditionSQL"