Rajout de doctrine/orm
[zf2.biz/galerie.git] / vendor / doctrine / orm / lib / Doctrine / ORM / Persisters / BasicEntityPersister.php
1 <?php
2 /*
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.
14  *
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>.
18  */
19
20 namespace Doctrine\ORM\Persisters;
21
22 use PDO;
23
24 use Doctrine\DBAL\LockMode;
25 use Doctrine\DBAL\Types\Type;
26 use Doctrine\DBAL\Connection;
27
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;
38
39 use Doctrine\Common\Util\ClassUtils;
40 use Doctrine\Common\Collections\Criteria;
41 use Doctrine\Common\Collections\Expr\Comparison;
42
43 /**
44  * A BasicEntityPersiter maps an entity to a single table in a relational database.
45  *
46  * A persister is always responsible for a single entity type.
47  *
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).
51  *
52  * The persisting operations that are invoked during a commit of a UnitOfWork to
53  * persist the persistent entity state are:
54  *
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.
59  *
60  * As can be seen from the above list, insertions are batched and executed all at once
61  * for increased efficiency.
62  *
63  * The querying operations invoked during a UnitOfWork, either through direct find
64  * requests or lazy-loading, are the following:
65  *
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).
71  *
72  * The BasicEntityPersister implementation provides the default behavior for
73  * persisting and querying entities that are mapped to a single database table.
74  *
75  * Subclasses can be created to provide custom persisting and querying strategies,
76  * i.e. spanning multiple tables.
77  *
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>
82  * @since 2.0
83  */
84 class BasicEntityPersister
85 {
86     /**
87      * @var array
88      */
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)',
99     );
100
101     /**
102      * Metadata object that describes the mapping of the mapped entity class.
103      *
104      * @var \Doctrine\ORM\Mapping\ClassMetadata
105      */
106     protected $_class;
107
108     /**
109      * The underlying DBAL Connection of the used EntityManager.
110      *
111      * @var \Doctrine\DBAL\Connection $conn
112      */
113     protected $_conn;
114
115     /**
116      * The database platform.
117      *
118      * @var \Doctrine\DBAL\Platforms\AbstractPlatform
119      */
120     protected $_platform;
121
122     /**
123      * The EntityManager instance.
124      *
125      * @var \Doctrine\ORM\EntityManager
126      */
127     protected $_em;
128
129     /**
130      * Queued inserts.
131      *
132      * @var array
133      */
134     protected $_queuedInserts = array();
135
136     /**
137      * ResultSetMapping that is used for all queries. Is generated lazily once per request.
138      *
139      * TODO: Evaluate Caching in combination with the other cached SQL snippets.
140      *
141      * @var Query\ResultSetMapping
142      */
143     protected $_rsm;
144
145     /**
146      * The map of column names to DBAL mapping types of all prepared columns used
147      * when INSERTing or UPDATEing an entity.
148      *
149      * @var array
150      * @see _prepareInsertData($entity)
151      * @see _prepareUpdateData($entity)
152      */
153     protected $_columnTypes = array();
154
155     /**
156      * The map of quoted column names.
157      *
158      * @var array
159      * @see _prepareInsertData($entity)
160      * @see _prepareUpdateData($entity)
161      */
162     protected $quotedColumns = array();
163
164     /**
165      * The INSERT SQL statement used for entities handled by this persister.
166      * This SQL is only generated once per request, if at all.
167      *
168      * @var string
169      */
170     private $_insertSql;
171
172     /**
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.
175      *
176      * @var string
177      */
178     protected $_selectColumnListSql;
179
180     /**
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.
183      *
184      * @var string
185      */
186     protected $_selectJoinSql;
187
188     /**
189      * Counter for creating unique SQL table and column aliases.
190      *
191      * @var integer
192      */
193     protected $_sqlAliasCounter = 0;
194
195     /**
196      * Map from class names (FQCN) to the corresponding generated SQL table aliases.
197      *
198      * @var array
199      */
200     protected $_sqlTableAliases = array();
201
202     /**
203      * The quote strategy.
204      *
205      * @var \Doctrine\ORM\Mapping\QuoteStrategy
206      */
207     protected $quoteStrategy;
208
209     /**
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.
212      *
213      * @param \Doctrine\ORM\EntityManager $em
214      * @param \Doctrine\ORM\Mapping\ClassMetadata $class
215      */
216     public function __construct(EntityManager $em, ClassMetadata $class)
217     {
218         $this->_em              = $em;
219         $this->_class           = $class;
220         $this->_conn            = $em->getConnection();
221         $this->_platform        = $this->_conn->getDatabasePlatform();
222         $this->quoteStrategy    = $em->getConfiguration()->getQuoteStrategy();
223     }
224
225     /**
226      * @return \Doctrine\ORM\Mapping\ClassMetadata
227      */
228     public function getClassMetadata()
229     {
230         return $this->_class;
231     }
232
233     /**
234      * Adds an entity to the queued insertions.
235      * The entity remains queued until {@link executeInserts} is invoked.
236      *
237      * @param object $entity The entity to queue for insertion.
238      */
239     public function addInsert($entity)
240     {
241         $this->_queuedInserts[spl_object_hash($entity)] = $entity;
242     }
243
244     /**
245      * Executes all queued entity insertions and returns any generated post-insert
246      * identifiers that were created as a result of the insertions.
247      *
248      * If no inserts are queued, invoking this method is a NOOP.
249      *
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.
252      */
253     public function executeInserts()
254     {
255         if ( ! $this->_queuedInserts) {
256             return;
257         }
258
259         $postInsertIds = array();
260         $idGen = $this->_class->idGenerator;
261         $isPostInsertId = $idGen->isPostInsertGenerator();
262
263         $stmt = $this->_conn->prepare($this->_getInsertSQL());
264         $tableName = $this->_class->getTableName();
265
266         foreach ($this->_queuedInserts as $entity) {
267             $insertData = $this->_prepareInsertData($entity);
268
269             if (isset($insertData[$tableName])) {
270                 $paramIndex = 1;
271
272                 foreach ($insertData[$tableName] as $column => $value) {
273                     $stmt->bindValue($paramIndex++, $value, $this->_columnTypes[$column]);
274                 }
275             }
276
277             $stmt->execute();
278
279             if ($isPostInsertId) {
280                 $id = $idGen->generate($this->_em, $entity);
281                 $postInsertIds[$id] = $entity;
282             } else {
283                 $id = $this->_class->getIdentifierValues($entity);
284             }
285
286             if ($this->_class->isVersioned) {
287                 $this->assignDefaultVersionValue($entity, $id);
288             }
289         }
290
291         $stmt->closeCursor();
292         $this->_queuedInserts = array();
293
294         return $postInsertIds;
295     }
296
297     /**
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.
301      *
302      * @param object $entity
303      * @param mixed $id
304      */
305     protected function assignDefaultVersionValue($entity, $id)
306     {
307         $value = $this->fetchVersionValue($this->_class, $id);
308         $this->_class->setFieldValue($entity, $this->_class->versionField, $value);
309     }
310
311     /**
312      * Fetch the current version value of a versioned entity.
313      *
314      * @param \Doctrine\ORM\Mapping\ClassMetadata $versionedClass
315      * @param mixed $id
316      * @return mixed
317      */
318     protected function fetchVersionValue($versionedClass, $id)
319     {
320         $versionField = $versionedClass->versionField;
321         $identifier   = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->_platform);
322
323         $versionFieldColumnName = $this->quoteStrategy->getColumnName($versionField, $versionedClass, $this->_platform);
324
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));
330
331         return Type::getType($versionedClass->fieldMappings[$versionField]['type'])->convertToPHPValue($value, $this->_platform);
332     }
333
334     /**
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.
337      *
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}.
341      *
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.
345      *
346      * @param object $entity The entity to update.
347      */
348     public function update($entity)
349     {
350         $updateData = $this->_prepareUpdateData($entity);
351         $tableName  = $this->_class->getTableName();
352
353         if (isset($updateData[$tableName]) && $updateData[$tableName]) {
354             $this->_updateTable(
355                 $entity, $this->quoteStrategy->getTableName($this->_class, $this->_platform),
356                 $updateData[$tableName], $this->_class->isVersioned
357             );
358
359             if ($this->_class->isVersioned) {
360                 $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
361                 $this->assignDefaultVersionValue($entity, $id);
362             }
363         }
364     }
365
366     /**
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.
369      *
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.
374      */
375     protected final function _updateTable($entity, $quotedTableName, array $updateData, $versioned = false)
376     {
377         $set = $params = $types = array();
378
379         foreach ($updateData as $columnName => $value) {
380             $column = $columnName;
381             $placeholder = '?';
382
383             if (isset($this->_class->fieldNames[$columnName])) {
384                 $column = $this->quoteStrategy->getColumnName($this->_class->fieldNames[$columnName], $this->_class, $this->_platform);
385
386                 if (isset($this->_class->fieldMappings[$this->_class->fieldNames[$columnName]]['requireSQLConversion'])) {
387                     $type = Type::getType($this->_columnTypes[$columnName]);
388                     $placeholder = $type->convertToDatabaseValueSQL('?', $this->_platform);
389                 }
390             } else if (isset($this->quotedColumns[$columnName])) {
391                 $column = $this->quotedColumns[$columnName];
392             }
393
394             $set[] = $column . ' = ' . $placeholder;
395             $params[] = $value;
396             $types[] = $this->_columnTypes[$columnName];
397         }
398
399         $where = array();
400         $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
401
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];
407
408                 switch (true) {
409                     case (isset($targetMapping->fieldMappings[$targetMapping->identifier[0]])):
410                         $types[] = $targetMapping->fieldMappings[$targetMapping->identifier[0]]['type'];
411                         break;
412
413                     case (isset($targetMapping->associationMappings[$targetMapping->identifier[0]])):
414                         $types[] = $targetMapping->associationMappings[$targetMapping->identifier[0]]['type'];
415                         break;
416
417                     default:
418                         throw ORMException::unrecognizedField($targetMapping->identifier[0]);
419                 }
420             } else {
421                 $where[] = $this->quoteStrategy->getColumnName($idField, $this->_class, $this->_platform);
422                 $params[] = $id[$idField];
423                 $types[] = $this->_class->fieldMappings[$idField]['type'];
424             }
425         }
426
427         if ($versioned) {
428             $versionField = $this->_class->versionField;
429             $versionFieldType = $this->_class->fieldMappings[$versionField]['type'];
430             $versionColumn = $this->quoteStrategy->getColumnName($versionField, $this->_class, $this->_platform);
431
432             if ($versionFieldType == Type::INTEGER) {
433                 $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1';
434             } else if ($versionFieldType == Type::DATETIME) {
435                 $set[] = $versionColumn . ' = CURRENT_TIMESTAMP';
436             }
437
438             $where[] = $versionColumn;
439             $params[] = $this->_class->reflFields[$versionField]->getValue($entity);
440             $types[] = $this->_class->fieldMappings[$versionField]['type'];
441         }
442
443         $sql = 'UPDATE ' . $quotedTableName
444              . ' SET ' . implode(', ', $set)
445              . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
446
447         $result = $this->_conn->executeUpdate($sql, $params, $types);
448
449         if ($versioned && ! $result) {
450             throw OptimisticLockException::lockFailed($entity);
451         }
452     }
453
454     /**
455      * @todo Add check for platform if it supports foreign keys/cascading.
456      * @param array $identifier
457      * @return void
458      */
459     protected function deleteJoinTableRecords($identifier)
460     {
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();
467                 $keys            = array();
468
469                 if ( ! $mapping['isOwningSide']) {
470                     $relatedClass   = $this->_em->getClassMetadata($mapping['targetEntity']);
471                     $mapping        = $relatedClass->associationMappings[$mapping['mappedBy']];
472
473                     foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
474                         $keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $relatedClass, $this->_platform);
475                     }
476
477                     if ($selfReferential) {
478                         foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
479                             $otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $relatedClass, $this->_platform);
480                         }
481                     }
482                 } else {
483
484                     foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
485                         $keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->_class, $this->_platform);
486                     }
487
488                     if ($selfReferential) {
489                         foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
490                             $otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->_class, $this->_platform);
491                         }
492                     }
493                 }
494
495                 if ( ! isset($mapping['isOnDeleteCascade'])) {
496
497                     $joinTableName = $this->quoteStrategy->getJoinTableName($mapping, $this->_class, $this->_platform);
498
499                     $this->_conn->delete($joinTableName, array_combine($keys, $identifier));
500
501                     if ($selfReferential) {
502                         $this->_conn->delete($joinTableName, array_combine($otherKeys, $identifier));
503                     }
504                 }
505             }
506         }
507     }
508
509     /**
510      * Deletes a managed entity.
511      *
512      * The entity to delete must be managed and have a persistent identifier.
513      * The deletion happens instantaneously.
514      *
515      * Subclasses may override this method to customize the semantics of entity deletion.
516      *
517      * @param object $entity The entity to delete.
518      */
519     public function delete($entity)
520     {
521         $identifier = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
522
523         $this->deleteJoinTableRecords($identifier);
524
525         $id = array_combine($this->quoteStrategy->getIdentifierColumnNames($this->_class, $this->_platform), $identifier);
526
527         $this->_conn->delete($this->quoteStrategy->getTableName($this->_class, $this->_platform), $id);
528     }
529
530     /**
531      * Prepares the changeset of an entity for database insertion (UPDATE).
532      *
533      * The changeset is obtained from the currently running UnitOfWork.
534      *
535      * During this preparation the array that is passed as the second parameter is filled with
536      * <columnName> => <value> pairs, grouped by table name.
537      *
538      * Example:
539      * <code>
540      * array(
541      *    'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...),
542      *    'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...),
543      *    ...
544      * )
545      * </code>
546      *
547      * @param object $entity The entity for which to prepare the data.
548      * @return array The prepared data.
549      */
550     protected function _prepareUpdateData($entity)
551     {
552         $result = array();
553         $uow = $this->_em->getUnitOfWork();
554
555         if (($versioned = $this->_class->isVersioned) != false) {
556             $versionField = $this->_class->versionField;
557         }
558
559         foreach ($uow->getEntityChangeSet($entity) as $field => $change) {
560             if ($versioned && $versionField == $field) {
561                 continue;
562             }
563
564             $newVal = $change[1];
565
566             if (isset($this->_class->associationMappings[$field])) {
567                 $assoc = $this->_class->associationMappings[$field];
568
569                 // Only owning side of x-1 associations can have a FK column.
570                 if ( ! $assoc['isOwningSide'] || ! ($assoc['type'] & ClassMetadata::TO_ONE)) {
571                     continue;
572                 }
573
574                 if ($newVal !== null) {
575                     $oid = spl_object_hash($newVal);
576
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)
583                         ));
584                         $newVal = null;
585                     }
586                 }
587
588                 if ($newVal !== null) {
589                     $newValId = $uow->getEntityIdentifier($newVal);
590                 }
591
592                 $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']);
593                 $owningTable = $this->getOwningTable($field);
594
595                 foreach ($assoc['joinColumns'] as $joinColumn) {
596                     $sourceColumn = $joinColumn['name'];
597                     $targetColumn = $joinColumn['referencedColumnName'];
598                     $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->_class, $this->_platform);
599
600                     $this->quotedColumns[$sourceColumn] = $quotedColumn;
601
602                     if ($newVal === null) {
603                         $result[$owningTable][$sourceColumn] = null;
604                     } else if ($targetClass->containsForeignIdentifier) {
605                         $result[$owningTable][$sourceColumn] = $newValId[$targetClass->getFieldForColumn($targetColumn)];
606                     } else {
607                         $result[$owningTable][$sourceColumn] = $newValId[$targetClass->fieldNames[$targetColumn]];
608                     }
609
610                     $this->_columnTypes[$sourceColumn] = $targetClass->getTypeOfColumn($targetColumn);
611                 }
612             } else {
613                 $columnName = $this->_class->columnNames[$field];
614                 $this->_columnTypes[$columnName] = $this->_class->fieldMappings[$field]['type'];
615                 $result[$this->getOwningTable($field)][$columnName] = $newVal;
616             }
617         }
618
619         return $result;
620     }
621
622     /**
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.
625      *
626      * The default insert data preparation is the same as for updates.
627      *
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
631      */
632     protected function _prepareInsertData($entity)
633     {
634         return $this->_prepareUpdateData($entity);
635     }
636
637     /**
638      * Gets the name of the table that owns the column the given field is mapped to.
639      *
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.
643      *
644      * @param string $fieldName The field name.
645      * @return string The table name.
646      */
647     public function getOwningTable($fieldName)
648     {
649         return $this->_class->getTableName();
650     }
651
652     /**
653      * Loads an entity by a list of field criteria.
654      *
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?
664      */
665     public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0, $limit = null)
666     {
667         $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, $lockMode, $limit);
668         list($params, $types) = $this->expandParameters($criteria);
669         $stmt = $this->_conn->executeQuery($sql, $params, $types);
670
671         if ($entity !== null) {
672             $hints[Query::HINT_REFRESH] = true;
673             $hints[Query::HINT_REFRESH_ENTITY] = $entity;
674         }
675
676         $hydrator = $this->_em->newHydrator($this->_selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
677         $entities = $hydrator->hydrateAll($stmt, $this->_rsm, $hints);
678
679         return $entities ? $entities[0] : null;
680     }
681
682     /**
683      * Loads an entity of this persister's mapped class as part of a single-valued
684      * association from another entity.
685      *
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.
692      */
693     public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = array())
694     {
695         if (($foundEntity = $this->_em->getUnitOfWork()->tryGetById($identifier, $assoc['targetEntity'])) != false) {
696             return $foundEntity;
697         }
698
699         $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']);
700
701         if ($assoc['isOwningSide']) {
702             $isInverseSingleValued = $assoc['inversedBy'] && ! $targetClass->isCollectionValuedAssociation($assoc['inversedBy']);
703
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).
706             $hints = array();
707
708             if ($isInverseSingleValued) {
709                 $hints['fetched']["r"][$assoc['inversedBy']] = true;
710             }
711
712             /* cascade read-only status
713             if ($this->_em->getUnitOfWork()->isReadOnly($sourceEntity)) {
714                 $hints[Query::HINT_READ_ONLY] = true;
715             }
716             */
717
718             $targetEntity = $this->load($identifier, null, $assoc, $hints);
719
720             // Complete bidirectional association, if necessary
721             if ($targetEntity !== null && $isInverseSingleValued) {
722                 $targetClass->reflFields[$assoc['inversedBy']]->setValue($targetEntity, $sourceEntity);
723             }
724         } else {
725             $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']);
726             $owningAssoc = $targetClass->getAssociationMapping($assoc['mappedBy']);
727
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
733                     );
734                 }
735
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);
740
741                 unset($identifier[$targetKeyColumn]);
742             }
743
744             $targetEntity = $this->load($identifier, null, $assoc);
745
746             if ($targetEntity !== null) {
747                 $targetClass->setFieldValue($targetEntity, $assoc['mappedBy'], $sourceEntity);
748             }
749         }
750
751         return $targetEntity;
752     }
753
754     /**
755      * Refreshes a managed entity.
756      *
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.
760      */
761     public function refresh(array $id, $entity, $lockMode = 0)
762     {
763         $sql = $this->_getSelectEntitiesSQL($id, null, $lockMode);
764         list($params, $types) = $this->expandParameters($id);
765         $stmt = $this->_conn->executeQuery($sql, $params, $types);
766
767         $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT);
768         $hydrator->hydrateAll($stmt, $this->_rsm, array(Query::HINT_REFRESH => true));
769     }
770
771     /**
772      * Load Entities matching the given Criteria object
773      *
774      * @param \Doctrine\Common\Collections\Criteria $criteria
775      *
776      * @return array
777      */
778     public function loadCriteria(Criteria $criteria)
779     {
780         $orderBy = $criteria->getOrderings();
781         $limit   = $criteria->getMaxResults();
782         $offset  = $criteria->getFirstResult();
783
784         $sql = $this->_getSelectEntitiesSQL($criteria, null, 0, $limit, $offset, $orderBy);
785
786         list($params, $types) = $this->expandCriteriaParameters($criteria);
787
788         $stmt = $this->_conn->executeQuery($sql, $params, $types);
789
790         $hydrator = $this->_em->newHydrator(($this->_selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
791
792         return $hydrator->hydrateAll($stmt, $this->_rsm, array('deferEagerLoads' => true));
793     }
794
795     /**
796      * Expand Criteria Parameters by walking the expressions and grabbing all
797      * parameters and types from it.
798      *
799      * @param \Doctrine\Common\Collections\Criteria $criteria
800      *
801      * @return array(array(), array())
802      */
803     private function expandCriteriaParameters(Criteria $criteria)
804     {
805         $expression = $criteria->getWhereExpression();
806
807         if ($expression === null) {
808             return array(array(), array());
809         }
810
811         $valueVisitor = new SqlValueVisitor();
812         $valueVisitor->dispatch($expression);
813
814         list($values, $types) = $valueVisitor->getParamsAndTypes();
815
816         $sqlValues = array();
817         foreach ($values as $value) {
818             $sqlValues[] = $this->getValue($value);
819         }
820
821         $sqlTypes = array();
822         foreach ($types as $type) {
823             list($field, $value) = $type;
824             $sqlTypes[] = $this->getType($field, $value);
825         }
826
827         return array($sqlValues, $sqlTypes);
828     }
829
830     /**
831      * Loads a list of entities by a list of field criteria.
832      *
833      * @param array $criteria
834      * @param array $orderBy
835      * @param int $limit
836      * @param int $offset
837      * @return array
838      */
839     public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null)
840     {
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);
844
845         $hydrator = $this->_em->newHydrator(($this->_selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
846
847         return $hydrator->hydrateAll($stmt, $this->_rsm, array('deferEagerLoads' => true));
848     }
849
850     /**
851      * Get (sliced or full) elements of the given collection.
852      *
853      * @param array $assoc
854      * @param object $sourceEntity
855      * @param int|null $offset
856      * @param int|null $limit
857      * @return array
858      */
859     public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
860     {
861         $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit);
862
863         return $this->loadArrayFromStatement($assoc, $stmt);
864     }
865
866     /**
867      * Load an array of entities from a given dbal statement.
868      *
869      * @param array $assoc
870      * @param \Doctrine\DBAL\Statement $stmt
871      *
872      * @return array
873      */
874     private function loadArrayFromStatement($assoc, $stmt)
875     {
876         $hints = array('deferEagerLoads' => true);
877
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']);
881         } else {
882             $rsm = $this->_rsm;
883         }
884
885         $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT);
886
887         return $hydrator->hydrateAll($stmt, $rsm, $hints);
888     }
889
890     /**
891      * Hydrate a collection from a given dbal statement.
892      *
893      * @param array $assoc
894      * @param \Doctrine\DBAL\Statement $stmt
895      * @param PersistentCollection $coll
896      *
897      * @return array
898      */
899     private function loadCollectionFromStatement($assoc, $stmt, $coll)
900     {
901         $hints = array('deferEagerLoads' => true, 'collection' => $coll);
902
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']);
906         } else {
907             $rsm = $this->_rsm;
908         }
909
910         $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT);
911
912         return $hydrator->hydrateAll($stmt, $rsm, $hints);
913     }
914
915     /**
916      * Loads a collection of entities of a many-to-many association.
917      *
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
923      * @return array
924      */
925     public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
926     {
927         $stmt = $this->getManyToManyStatement($assoc, $sourceEntity);
928
929         return $this->loadCollectionFromStatement($assoc, $stmt, $coll);
930     }
931
932     private function getManyToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
933     {
934         $criteria = array();
935         $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']);
936
937         if ($assoc['isOwningSide']) {
938             $quotedJoinTable = $this->quoteStrategy->getJoinTableName($assoc, $sourceClass, $this->_platform);
939
940             foreach ($assoc['joinTable']['joinColumns'] as $joinColumn) {
941                 $relationKeyColumn  = $joinColumn['name'];
942                 $sourceKeyColumn    = $joinColumn['referencedColumnName'];
943                 $quotedKeyColumn    = $this->quoteStrategy->getJoinColumnName($joinColumn, $sourceClass, $this->_platform);
944
945                 if ($sourceClass->containsForeignIdentifier) {
946                     $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
947                     $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
948
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]];
952                     }
953
954                     $criteria[$quotedJoinTable . "." . $quotedKeyColumn] = $value;
955                 } else if (isset($sourceClass->fieldNames[$sourceKeyColumn])) {
956                     $criteria[$quotedJoinTable . "." . $quotedKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
957                 } else {
958                     throw MappingException::joinColumnMustPointToMappedField(
959                         $sourceClass->name, $sourceKeyColumn
960                     );
961                 }
962             }
963         } else {
964             $owningAssoc = $this->_em->getClassMetadata($assoc['targetEntity'])->associationMappings[$assoc['mappedBy']];
965             $quotedJoinTable = $this->quoteStrategy->getJoinTableName($owningAssoc, $sourceClass, $this->_platform);
966
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);
972
973                 if ($sourceClass->containsForeignIdentifier) {
974                     $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
975                     $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
976
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]];
980                     }
981
982                     $criteria[$quotedJoinTable . "." . $quotedKeyColumn] = $value;
983                 } else if (isset($sourceClass->fieldNames[$sourceKeyColumn])) {
984                     $criteria[$quotedJoinTable . "." . $quotedKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
985                 } else {
986                     throw MappingException::joinColumnMustPointToMappedField(
987                         $sourceClass->name, $sourceKeyColumn
988                     );
989                 }
990             }
991         }
992
993         $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset);
994         list($params, $types) = $this->expandParameters($criteria);
995
996         return $this->_conn->executeQuery($sql, $params, $types);
997     }
998
999     /**
1000      * Gets the SELECT SQL to select one or more entities by a set of field criteria.
1001      *
1002      * @param array|\Doctrine\Common\Collections\Criteria $criteria
1003      * @param AssociationMapping $assoc
1004      * @param string $orderBy
1005      * @param int $lockMode
1006      * @param int $limit
1007      * @param int $offset
1008      * @param array $orderBy
1009      * @return string
1010      * @todo Refactor: _getSelectSQL(...)
1011      */
1012     protected function _getSelectEntitiesSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null)
1013     {
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);
1018
1019         $orderBy    = ($assoc !== null && isset($assoc['orderBy'])) ? $assoc['orderBy'] : $orderBy;
1020         $orderBySql = $orderBy ? $this->_getOrderBySQL($orderBy, $this->_getSQLTableAlias($this->_class->name)) : '';
1021
1022         $lockSql = '';
1023
1024         if ($lockMode == LockMode::PESSIMISTIC_READ) {
1025             $lockSql = ' ' . $this->_platform->getReadLockSql();
1026         } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) {
1027             $lockSql = ' ' . $this->_platform->getWriteLockSql();
1028         }
1029
1030         $alias = $this->_getSQLTableAlias($this->_class->name);
1031
1032         if ($filterSql = $this->generateFilterConditionSQL($this->_class, $alias)) {
1033             if ($conditionSql) {
1034                 $conditionSql .= ' AND ';
1035             }
1036
1037             $conditionSql .= $filterSql;
1038         }
1039
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)
1046              . $lockSql;
1047     }
1048
1049     /**
1050      * Gets the ORDER BY SQL snippet for ordered collections.
1051      *
1052      * @param array $orderBy
1053      * @param string $baseTableAlias
1054      * @return string
1055      */
1056     protected final function _getOrderBySQL(array $orderBy, $baseTableAlias)
1057     {
1058         $orderBySql = '';
1059
1060         foreach ($orderBy as $fieldName => $orientation) {
1061             if ( ! isset($this->_class->fieldMappings[$fieldName])) {
1062                 throw ORMException::unrecognizedField($fieldName);
1063             }
1064
1065             $orientation = strtoupper(trim($orientation));
1066             if ($orientation != 'ASC' && $orientation != 'DESC') {
1067                 throw ORMException::invalidOrientation($this->_class->name, $fieldName);
1068             }
1069
1070             $tableAlias = isset($this->_class->fieldMappings[$fieldName]['inherited']) ?
1071                     $this->_getSQLTableAlias($this->_class->fieldMappings[$fieldName]['inherited'])
1072                     : $baseTableAlias;
1073
1074             $columnName = $this->quoteStrategy->getColumnName($fieldName, $this->_class, $this->_platform);
1075
1076             $orderBySql .= $orderBySql ? ', ' : ' ORDER BY ';
1077             $orderBySql .= $tableAlias . '.' . $columnName . ' ' . $orientation;
1078         }
1079
1080         return $orderBySql;
1081     }
1082
1083     /**
1084      * Gets the SQL fragment with the list of columns to select when querying for
1085      * an entity in this persister.
1086      *
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.
1091      *
1092      * @return string The SQL fragment.
1093      * @todo Rename: _getSelectColumnsSQL()
1094      */
1095     protected function _getSelectColumnListSQL()
1096     {
1097         if ($this->_selectColumnListSql !== null) {
1098             return $this->_selectColumnListSql;
1099         }
1100
1101         $columnList = '';
1102         $this->_rsm = new Query\ResultSetMapping();
1103         $this->_rsm->addEntityResult($this->_class->name, 'r'); // r for root
1104
1105         // Add regular columns to select list
1106         foreach ($this->_class->fieldNames as $field) {
1107             if ($columnList) $columnList .= ', ';
1108
1109             $columnList .= $this->_getSelectColumnSQL($field, $this->_class);
1110         }
1111
1112         $this->_selectJoinSql = '';
1113         $eagerAliasCounter = 0;
1114
1115         foreach ($this->_class->associationMappings as $assocField => $assoc) {
1116             $assocColumnSQL = $this->_getSelectColumnAssociationSQL($assocField, $assoc, $this->_class);
1117
1118             if ($assocColumnSQL) {
1119                 if ($columnList) $columnList .= ', ';
1120
1121                 $columnList .= $assocColumnSQL;
1122             }
1123
1124             if ($assoc['type'] & ClassMetadata::TO_ONE && ($assoc['fetch'] == ClassMetadata::FETCH_EAGER || !$assoc['isOwningSide'])) {
1125                 $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']);
1126
1127                 if ($eagerEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) {
1128                     continue; // now this is why you shouldn't use inheritance
1129                 }
1130
1131                 $assocAlias = 'e' . ($eagerAliasCounter++);
1132                 $this->_rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField);
1133
1134                 foreach ($eagerEntity->fieldNames as $field) {
1135                     if ($columnList) $columnList .= ', ';
1136
1137                     $columnList .= $this->_getSelectColumnSQL($field, $eagerEntity, $assocAlias);
1138                 }
1139
1140                 foreach ($eagerEntity->associationMappings as $assoc2Field => $assoc2) {
1141                     $assoc2ColumnSQL = $this->_getSelectColumnAssociationSQL($assoc2Field, $assoc2, $eagerEntity, $assocAlias);
1142
1143                     if ($assoc2ColumnSQL) {
1144                         if ($columnList) $columnList .= ', ';
1145                         $columnList .= $assoc2ColumnSQL;
1146                     }
1147                 }
1148                 $first = true;
1149
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 ';
1153
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);
1158
1159                         if ( ! $first) {
1160                             $this->_selectJoinSql .= ' AND ';
1161                         }
1162                         $this->_selectJoinSql .= $this->_getSQLTableAlias($assoc['sourceEntity']) . '.' . $sourceCol . ' = '
1163                                                . $tableAlias . '.' . $targetCol;
1164                         $first = false;
1165                     }
1166
1167                     // Add filter SQL
1168                     if ($filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias)) {
1169                         $this->_selectJoinSql .= ' AND ' . $filterSql;
1170                     }
1171                 } else {
1172                     $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']);
1173                     $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']);
1174
1175                     $this->_selectJoinSql .= ' LEFT JOIN';
1176                     $this->_selectJoinSql .= ' ' . $this->quoteStrategy->getTableName($eagerEntity, $this->_platform) . ' '
1177                                            . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) . ' ON ';
1178
1179                     foreach ($owningAssoc['sourceToTargetKeyColumns'] as $sourceCol => $targetCol) {
1180                         if ( ! $first) {
1181                             $this->_selectJoinSql .= ' AND ';
1182                         }
1183
1184                         $this->_selectJoinSql .= $this->_getSQLTableAlias($owningAssoc['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = '
1185                                                . $this->_getSQLTableAlias($owningAssoc['targetEntity']) . '.' . $targetCol;
1186                         $first = false;
1187                     }
1188                 }
1189             }
1190         }
1191
1192         $this->_selectColumnListSql = $columnList;
1193
1194         return $this->_selectColumnListSql;
1195     }
1196
1197     /**
1198      * Gets the SQL join fragment used when selecting entities from an association.
1199      *
1200      * @param string $field
1201      * @param array $assoc
1202      * @param ClassMetadata $class
1203      * @param string $alias
1204      *
1205      * @return string
1206      */
1207     protected function _getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r')
1208     {
1209         $columnList = array();
1210
1211         if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
1212
1213             foreach ($assoc['joinColumns'] as $joinColumn) {
1214
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;
1219
1220                 $this->_rsm->addMetaResult($alias, $resultColumnName, $quotedColumn, isset($assoc['id']) && $assoc['id'] === true);
1221             }
1222         }
1223
1224         return implode(', ', $columnList);
1225     }
1226
1227     /**
1228      * Gets the SQL join fragment used when selecting entities from a
1229      * many-to-many association.
1230      *
1231      * @param ManyToManyMapping $manyToMany
1232      * @return string
1233      */
1234     protected function _getSelectManyToManyJoinSQL(array $manyToMany)
1235     {
1236         $conditions         = array();
1237         $association        = $manyToMany;
1238         $sourceTableAlias   = $this->_getSQLTableAlias($this->_class->name);
1239
1240         if ( ! $manyToMany['isOwningSide']) {
1241             $targetEntity   = $this->_em->getClassMetadata($manyToMany['targetEntity']);
1242             $association    = $targetEntity->associationMappings[$manyToMany['mappedBy']];
1243         }
1244
1245         $joinTableName  = $this->quoteStrategy->getJoinTableName($association, $this->_class, $this->_platform);
1246         $joinColumns    = ($manyToMany['isOwningSide'])
1247             ? $association['joinTable']['inverseJoinColumns']
1248             : $association['joinTable']['joinColumns'];
1249
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;
1254         }
1255
1256         return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
1257     }
1258
1259     /**
1260      * Gets the INSERT SQL used by the persister to persist a new entity.
1261      *
1262      * @return string
1263      */
1264     protected function _getInsertSQL()
1265     {
1266         if ($this->_insertSql === null) {
1267             $insertSql = '';
1268             $columns = $this->_getInsertColumnList();
1269
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)
1274                 );
1275             } else {
1276                 $columns = array_unique($columns);
1277
1278                 $values = array();
1279                 foreach ($columns as $column) {
1280                     $placeholder = '?';
1281
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);
1287                     }
1288
1289                     $values[] = $placeholder;
1290                 }
1291
1292                 $insertSql = 'INSERT INTO ' . $this->quoteStrategy->getTableName($this->_class, $this->_platform)
1293                         . ' (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $values) . ')';
1294             }
1295
1296             $this->_insertSql = $insertSql;
1297         }
1298
1299         return $this->_insertSql;
1300     }
1301
1302     /**
1303      * Gets the list of columns to put in the INSERT SQL statement.
1304      *
1305      * Subclasses should override this method to alter or change the list of
1306      * columns placed in the INSERT statements used by the persister.
1307      *
1308      * @return array The list of columns.
1309      */
1310     protected function _getInsertColumnList()
1311     {
1312         $columns = array();
1313
1314         foreach ($this->_class->reflFields as $name => $field) {
1315             if ($this->_class->isVersioned && $this->_class->versionField == $name) {
1316                 continue;
1317             }
1318
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);
1324                     }
1325                 }
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'];
1329             }
1330         }
1331
1332         return $columns;
1333     }
1334
1335     /**
1336      * Gets the SQL snippet of a qualified column name for the given field name.
1337      *
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
1342      */
1343     protected function _getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
1344     {
1345         $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias)
1346              . '.' . $this->quoteStrategy->getColumnName($field, $class, $this->_platform);
1347         $columnAlias = $this->getSQLColumnAlias($class->columnNames[$field]);
1348
1349         $this->_rsm->addFieldResult($alias, $columnAlias, $field);
1350
1351         if (isset($class->fieldMappings[$field]['requireSQLConversion'])) {
1352             $type = Type::getType($class->getTypeOfField($field));
1353             $sql = $type->convertToPHPValueSQL($sql, $this->_platform);
1354         }
1355
1356         return $sql . ' AS ' . $columnAlias;
1357     }
1358
1359     /**
1360      * Gets the SQL table alias for the given class name.
1361      *
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.
1365      */
1366     protected function _getSQLTableAlias($className, $assocName = '')
1367     {
1368         if ($assocName) {
1369             $className .= '#' . $assocName;
1370         }
1371
1372         if (isset($this->_sqlTableAliases[$className])) {
1373             return $this->_sqlTableAliases[$className];
1374         }
1375
1376         $tableAlias = 't' . $this->_sqlAliasCounter++;
1377
1378         $this->_sqlTableAliases[$className] = $tableAlias;
1379
1380         return $tableAlias;
1381     }
1382
1383     /**
1384      * Lock all rows of this entity matching the given criteria with the specified pessimistic lock mode
1385      *
1386      * @param array $criteria
1387      * @param int $lockMode
1388      * @return void
1389      */
1390     public function lock(array $criteria, $lockMode)
1391     {
1392         $conditionSql = $this->_getSelectConditionSQL($criteria);
1393
1394         if ($lockMode == LockMode::PESSIMISTIC_READ) {
1395             $lockSql = $this->_platform->getReadLockSql();
1396         } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) {
1397             $lockSql = $this->_platform->getWriteLockSql();
1398         }
1399
1400         $sql = 'SELECT 1 '
1401              . $this->_platform->appendLockHint($this->getLockTablesSql(), $lockMode)
1402              . ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ' . $lockSql;
1403
1404         list($params, $types) = $this->expandParameters($criteria);
1405
1406         $this->_conn->executeQuery($sql, $params, $types);
1407     }
1408
1409     /**
1410      * Get the FROM and optionally JOIN conditions to lock the entity managed by this persister.
1411      *
1412      * @return string
1413      */
1414     protected function getLockTablesSql()
1415     {
1416         return 'FROM ' . $this->quoteStrategy->getTableName($this->_class, $this->_platform) . ' '
1417              . $this->_getSQLTableAlias($this->_class->name);
1418     }
1419
1420     /**
1421      * Get the Select Where Condition from a Criteria object.
1422      *
1423      * @param \Doctrine\Common\Collections\Criteria $criteria
1424      * @return string
1425      */
1426     protected function _getSelectConditionCriteriaSQL(Criteria $criteria)
1427     {
1428         $expression = $criteria->getWhereExpression();
1429
1430         if ($expression === null) {
1431             return '';
1432         }
1433
1434         $visitor = new SqlExpressionVisitor($this);
1435
1436         return $visitor->dispatch($expression);
1437     }
1438
1439     /**
1440      * Get the SQL WHERE condition for matching a field with a given value.
1441      *
1442      * @param string $field
1443      * @param mixed $value
1444      * @param array|null $assoc
1445      * @param string $comparison
1446      *
1447      * @return string
1448      */
1449     public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null)
1450     {
1451         $conditionSql = $this->getSelectConditionStatementColumnSQL($field, $assoc);
1452         $placeholder  = '?';
1453
1454         if (isset($this->_class->fieldMappings[$field]['requireSQLConversion'])) {
1455             $type = Type::getType($this->_class->getTypeOfField($field));
1456             $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->_platform);
1457         }
1458
1459         $conditionSql .= ($comparison === null)
1460             ? ((is_array($value)) ? ' IN (?)' : (($value === null) ? ' IS NULL' : ' = ' . $placeholder))
1461             : ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
1462
1463
1464         return $conditionSql;
1465     }
1466
1467     /**
1468      * Build the left-hand-side of a where condition statement.
1469      *
1470      * @param string $field
1471      * @param array $assoc
1472      *
1473      * @return string
1474      */
1475     protected function getSelectConditionStatementColumnSQL($field, $assoc = null)
1476     {
1477         switch (true) {
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;
1482
1483                 return $this->_getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->_class, $this->_platform);
1484
1485             case (isset($this->_class->associationMappings[$field])):
1486                 if ( ! $this->_class->associationMappings[$field]['isOwningSide']) {
1487                     throw ORMException::invalidFindByInverseAssociation($this->_class->name, $field);
1488                 }
1489
1490                 $className = (isset($this->_class->associationMappings[$field]['inherited']))
1491                     ? $this->_class->associationMappings[$field]['inherited']
1492                     : $this->_class->name;
1493
1494                 return $this->_getSQLTableAlias($className) . '.' . $this->_class->associationMappings[$field]['joinColumns'][0]['name'];
1495
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.
1499
1500                 // found a join column condition, not really a "field"
1501                 return $field;
1502         }
1503
1504         throw ORMException::unrecognizedField($field);
1505     }
1506
1507     /**
1508      * Gets the conditional SQL fragment used in the WHERE clause when selecting
1509      * entities in this persister.
1510      *
1511      * Subclasses are supposed to override this method if they intend to change
1512      * or alter the criteria by which entities are selected.
1513      *
1514      * @param array $criteria
1515      * @param AssociationMapping $assoc
1516      * @return string
1517      */
1518     protected function _getSelectConditionSQL(array $criteria, $assoc = null)
1519     {
1520         $conditionSql = '';
1521
1522         foreach ($criteria as $field => $value) {
1523             $conditionSql .= $conditionSql ? ' AND ' : '';
1524             $conditionSql .= $this->getSelectConditionStatementSQL($field, $value, $assoc);
1525         }
1526
1527         return $conditionSql;
1528     }
1529
1530     /**
1531      * Return an array with (sliced or full list) of elements in the specified collection.
1532      *
1533      * @param array $assoc
1534      * @param object $sourceEntity
1535      * @param int $offset
1536      * @param int $limit
1537      * @return array
1538      */
1539     public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
1540     {
1541         $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit);
1542
1543         return $this->loadArrayFromStatement($assoc, $stmt);
1544     }
1545
1546     /**
1547      * Loads a collection of entities in a one-to-many association.
1548      *
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
1554      */
1555     public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
1556     {
1557         $stmt = $this->getOneToManyStatement($assoc, $sourceEntity);
1558
1559         return $this->loadCollectionFromStatement($assoc, $stmt, $coll);
1560     }
1561
1562     /**
1563      * Build criteria and execute SQL statement to fetch the one to many entities from.
1564      *
1565      * @param array $assoc
1566      * @param object $sourceEntity
1567      * @param int|null $offset
1568      * @param int|null $limit
1569      * @return \Doctrine\DBAL\Statement
1570      */
1571     private function getOneToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
1572     {
1573         $criteria = array();
1574         $owningAssoc = $this->_class->associationMappings[$assoc['mappedBy']];
1575         $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']);
1576
1577         $tableAlias = $this->_getSQLTableAlias(isset($owningAssoc['inherited']) ? $owningAssoc['inherited'] : $this->_class->name);
1578
1579         foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
1580             if ($sourceClass->containsForeignIdentifier) {
1581                 $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
1582                 $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1583
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]];
1587                 }
1588
1589                 $criteria[$tableAlias . "." . $targetKeyColumn] = $value;
1590             } else {
1591                 $criteria[$tableAlias . "." . $targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
1592             }
1593         }
1594
1595         $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset);
1596         list($params, $types) = $this->expandParameters($criteria);
1597
1598         return $this->_conn->executeQuery($sql, $params, $types);
1599     }
1600
1601     /**
1602      * Expand the parameters from the given criteria and use the correct binding types if found.
1603      *
1604      * @param  array $criteria
1605      * @return array
1606      */
1607     private function expandParameters($criteria)
1608     {
1609         $params = $types = array();
1610
1611         foreach ($criteria as $field => $value) {
1612             if ($value === null) {
1613                 continue; // skip null values.
1614             }
1615
1616             $types[]  = $this->getType($field, $value);
1617             $params[] = $this->getValue($value);
1618         }
1619
1620         return array($params, $types);
1621     }
1622
1623     /**
1624      * Infer field type to be used by parameter type casting.
1625      *
1626      * @param string $field
1627      * @param mixed $value
1628      * @return integer
1629      */
1630     private function getType($field, $value)
1631     {
1632         switch (true) {
1633             case (isset($this->_class->fieldMappings[$field])):
1634                 $type = $this->_class->fieldMappings[$field]['type'];
1635                 break;
1636
1637             case (isset($this->_class->associationMappings[$field])):
1638                 $assoc = $this->_class->associationMappings[$field];
1639
1640                 if (count($assoc['sourceToTargetKeyColumns']) > 1) {
1641                     throw Query\QueryException::associationPathCompositeKeyNotSupported();
1642                 }
1643
1644                 $targetClass  = $this->_em->getClassMetadata($assoc['targetEntity']);
1645                 $targetColumn = $assoc['joinColumns'][0]['referencedColumnName'];
1646                 $type         = null;
1647
1648                 if (isset($targetClass->fieldNames[$targetColumn])) {
1649                     $type = $targetClass->fieldMappings[$targetClass->fieldNames[$targetColumn]]['type'];
1650                 }
1651
1652                 break;
1653
1654             default:
1655                 $type = null;
1656         }
1657         if (is_array($value)) {
1658             $type = Type::getType( $type )->getBindingType();
1659             $type += Connection::ARRAY_PARAM_OFFSET;
1660         }
1661
1662         return $type;
1663     }
1664
1665     /**
1666      * Retrieve parameter value
1667      *
1668      * @param mixed $value
1669      * @return mixed
1670      */
1671     private function getValue($value)
1672     {
1673         if (is_array($value)) {
1674             $newValue = array();
1675
1676             foreach ($value as $itemValue) {
1677                 $newValue[] = $this->getIndividualValue($itemValue);
1678             }
1679
1680             return $newValue;
1681         }
1682
1683         return $this->getIndividualValue($value);
1684     }
1685
1686     /**
1687      * Retrieve an invidiual parameter value
1688      *
1689      * @param mixed $value
1690      * @return mixed
1691      */
1692     private function getIndividualValue($value)
1693     {
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);
1697             } else {
1698                 $class = $this->_em->getClassMetadata(get_class($value));
1699                 $idValues = $class->getIdentifierValues($value);
1700             }
1701
1702             $value = $idValues[key($idValues)];
1703         }
1704
1705         return $value;
1706     }
1707
1708     /**
1709      * Checks whether the given managed entity exists in the database.
1710      *
1711      * @param object $entity
1712      * @return boolean TRUE if the entity exists in the database, FALSE otherwise.
1713      */
1714     public function exists($entity, array $extraConditions = array())
1715     {
1716         $criteria = $this->_class->getIdentifierValues($entity);
1717
1718         if ( ! $criteria) {
1719             return false;
1720         }
1721
1722         if ($extraConditions) {
1723             $criteria = array_merge($criteria, $extraConditions);
1724         }
1725
1726         $alias = $this->_getSQLTableAlias($this->_class->name);
1727
1728         $sql = 'SELECT 1 '
1729              . $this->getLockTablesSql()
1730              . ' WHERE ' . $this->_getSelectConditionSQL($criteria);
1731
1732         if ($filterSql = $this->generateFilterConditionSQL($this->_class, $alias)) {
1733             $sql .= ' AND ' . $filterSql;
1734         }
1735
1736         list($params) = $this->expandParameters($criteria);
1737
1738         return (bool) $this->_conn->fetchColumn($sql, $params);
1739     }
1740
1741     /**
1742      * Generates the appropriate join SQL for the given join column.
1743      *
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.
1746      */
1747     protected function getJoinSQLForJoinColumns($joinColumns)
1748     {
1749         // if one of the join columns is nullable, return left join
1750         foreach ($joinColumns as $joinColumn) {
1751              if ( ! isset($joinColumn['nullable']) || $joinColumn['nullable']) {
1752                  return 'LEFT JOIN';
1753              }
1754         }
1755
1756         return 'INNER JOIN';
1757     }
1758
1759     /**
1760      * Gets an SQL column alias for a column name.
1761      *
1762      * @param string $columnName
1763      * @return string
1764      */
1765     public function getSQLColumnAlias($columnName)
1766     {
1767         return $this->quoteStrategy->getColumnAlias($columnName, $this->_sqlAliasCounter++, $this->_platform);
1768     }
1769
1770     /**
1771      * Generates the filter SQL for a given entity and table alias.
1772      *
1773      * @param ClassMetadata $targetEntity Metadata of the target entity.
1774      * @param string $targetTableAlias The table alias of the joined/selected table.
1775      *
1776      * @return string The SQL query part to add to a query.
1777      */
1778     protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
1779     {
1780         $filterClauses = array();
1781
1782         foreach ($this->_em->getFilters()->getEnabledFilters() as $filter) {
1783             if ('' !== $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
1784                 $filterClauses[] = '(' . $filterExpr . ')';
1785             }
1786         }
1787
1788         $sql = implode(' AND ', $filterClauses);
1789         return $sql ? "(" . $sql . ")" : ""; // Wrap again to avoid "X or Y and FilterConditionSQL"
1790     }
1791 }