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

namespace Doctrine\ORM\Persisters;

use Doctrine\ORM\PersistentCollection,
    Doctrine\ORM\UnitOfWork;

/**
 * Persister for many-to-many collections.
 *
 * @author Roman Borschel <roman@code-factory.org>
 * @since 2.0
 */
class ManyToManyPersister extends AbstractCollectionPersister
{
    /**
     * {@inheritdoc}
     *
     * @override
     */
    protected function _getDeleteRowSQL(PersistentCollection $coll)
    {
        $mapping = $coll->getMapping();
        $joinTable = $mapping['joinTable'];
        $columns = $mapping['joinTableColumns'];
        return 'DELETE FROM ' . $joinTable['name'] . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
    }

    /**
     * {@inheritdoc}
     *
     * @override
     * @internal Order of the parameters must be the same as the order of the columns in
     *           _getDeleteRowSql.
     */
    protected function _getDeleteRowSQLParameters(PersistentCollection $coll, $element)
    {
        return $this->_collectJoinTableColumnParameters($coll, $element);
    }

    /**
     * {@inheritdoc}
     *
     * @override
     */
    protected function _getUpdateRowSQL(PersistentCollection $coll)
    {}

    /**
     * {@inheritdoc}
     *
     * @override
     * @internal Order of the parameters must be the same as the order of the columns in
     *           _getInsertRowSql.
     */
    protected function _getInsertRowSQL(PersistentCollection $coll)
    {
        $mapping = $coll->getMapping();
        $joinTable = $mapping['joinTable'];
        $columns = $mapping['joinTableColumns'];
        return 'INSERT INTO ' . $joinTable['name'] . ' (' . implode(', ', $columns) . ')'
                . ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')';
    }

    /**
     * {@inheritdoc}
     *
     * @override
     * @internal Order of the parameters must be the same as the order of the columns in
     *           _getInsertRowSql.
     */
    protected function _getInsertRowSQLParameters(PersistentCollection $coll, $element)
    {
        return $this->_collectJoinTableColumnParameters($coll, $element);
    }

    /**
     * Collects the parameters for inserting/deleting on the join table in the order
     * of the join table columns as specified in ManyToManyMapping#joinTableColumns.
     *
     * @param $coll
     * @param $element
     * @return array
     */
    private function _collectJoinTableColumnParameters(PersistentCollection $coll, $element)
    {
        $params = array();
        $mapping = $coll->getMapping();
        $isComposite = count($mapping['joinTableColumns']) > 2;

        $identifier1 = $this->_uow->getEntityIdentifier($coll->getOwner());
        $identifier2 = $this->_uow->getEntityIdentifier($element);

        if ($isComposite) {
            $class1 = $this->_em->getClassMetadata(get_class($coll->getOwner()));
            $class2 = $coll->getTypeClass();
        }

        foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
            if (isset($mapping['relationToSourceKeyColumns'][$joinTableColumn])) {
                if ($isComposite) {
                    if ($class1->containsForeignIdentifier) {
                        $params[] = $identifier1[$class1->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])];
                    } else {
                        $params[] = $identifier1[$class1->fieldNames[$mapping['relationToSourceKeyColumns'][$joinTableColumn]]];
                    }
                } else {
                    $params[] = array_pop($identifier1);
                }
            } else {
                if ($isComposite) {
                    if ($class2->containsForeignIdentifier) {
                        $params[] = $identifier2[$class2->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])];
                    } else {
                        $params[] = $identifier2[$class2->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]];
                    }
                } else {
                    $params[] = array_pop($identifier2);
                }
            }
        }

        return $params;
    }

    /**
     * {@inheritdoc}
     *
     * @override
     */
    protected function _getDeleteSQL(PersistentCollection $coll)
    {
        $mapping = $coll->getMapping();
        $joinTable = $mapping['joinTable'];
        $whereClause = '';
        foreach ($mapping['relationToSourceKeyColumns'] as $relationColumn => $srcColumn) {
            if ($whereClause !== '') $whereClause .= ' AND ';
            $whereClause .= "$relationColumn = ?";
        }
        return 'DELETE FROM ' . $joinTable['name'] . ' WHERE ' . $whereClause;
    }

    /**
     * {@inheritdoc}
     *
     * @override
     * @internal Order of the parameters must be the same as the order of the columns in
     *           _getDeleteSql.
     */
    protected function _getDeleteSQLParameters(PersistentCollection $coll)
    {
        $params = array();
        $mapping = $coll->getMapping();
        $identifier = $this->_uow->getEntityIdentifier($coll->getOwner());
        if (count($mapping['relationToSourceKeyColumns']) > 1) {
            $sourceClass = $this->_em->getClassMetadata(get_class($mapping->getOwner()));
            foreach ($mapping['relationToSourceKeyColumns'] as $relColumn => $srcColumn) {
                $params[] = $identifier[$sourceClass->fieldNames[$srcColumn]];
            }
        } else {
           $params[] = array_pop($identifier);
        }

        return $params;
    }

    /**
     * {@inheritdoc}
     */
    public function count(PersistentCollection $coll)
    {
        $params = array();
        $mapping = $coll->getMapping();
        $class = $this->_em->getClassMetadata($mapping['sourceEntity']);
        $id = $this->_em->getUnitOfWork()->getEntityIdentifier($coll->getOwner());

        if ($mapping['isOwningSide']) {
            $joinTable = $mapping['joinTable'];
            $joinColumns = $mapping['relationToSourceKeyColumns'];
        } else {
            $mapping = $this->_em->getClassMetadata($mapping['targetEntity'])->associationMappings[$mapping['mappedBy']];
            $joinTable = $mapping['joinTable'];
            $joinColumns = $mapping['relationToTargetKeyColumns'];
        }

        $whereClause = '';
        foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
            if (isset($joinColumns[$joinTableColumn])) {
                if ($whereClause !== '') {
                    $whereClause .= ' AND ';
                }
                $whereClause .= "$joinTableColumn = ?";

                if ($class->containsForeignIdentifier) {
                    $params[] = $id[$class->getFieldForColumn($joinColumns[$joinTableColumn])];
                } else {
                    $params[] = $id[$class->fieldNames[$joinColumns[$joinTableColumn]]];
                }
            }
        }
        $sql = 'SELECT count(*) FROM ' . $joinTable['name'] . ' WHERE ' . $whereClause;

        return $this->_conn->fetchColumn($sql, $params);
    }

    /**
     * @param PersistentCollection $coll
     * @param int $offset
     * @param int $length
     * @return array
     */
    public function slice(PersistentCollection $coll, $offset, $length = null)
    {
        $mapping = $coll->getMapping();
        return $this->_em->getUnitOfWork()
                  ->getEntityPersister($mapping['targetEntity'])
                  ->getManyToManyCollection($mapping, $coll->getOwner(), $offset, $length);
    }

    /**
     * @param PersistentCollection $coll
     * @param object $element
     */
    public function contains(PersistentCollection $coll, $element)
    {
        $uow = $this->_em->getUnitOfWork();

        // shortcut for new entities
        if ($uow->getEntityState($element, UnitOfWork::STATE_NEW) == UnitOfWork::STATE_NEW) {
            return false;
        }

        $params = array();
        $mapping = $coll->getMapping();

        if (!$mapping['isOwningSide']) {
            $sourceClass = $this->_em->getClassMetadata($mapping['targetEntity']);
            $targetClass = $this->_em->getClassMetadata($mapping['sourceEntity']);
            $sourceId = $uow->getEntityIdentifier($element);
            $targetId = $uow->getEntityIdentifier($coll->getOwner());

            $mapping = $sourceClass->associationMappings[$mapping['mappedBy']];
        } else {
            $sourceClass = $this->_em->getClassMetadata($mapping['sourceEntity']);
            $targetClass = $this->_em->getClassMetadata($mapping['targetEntity']);
            $sourceId = $uow->getEntityIdentifier($coll->getOwner());
            $targetId = $uow->getEntityIdentifier($element);
        }
        $joinTable = $mapping['joinTable'];

        $whereClause = '';
        foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
            if (isset($mapping['relationToTargetKeyColumns'][$joinTableColumn])) {
                if ($whereClause !== '') {
                    $whereClause .= ' AND ';
                }
                $whereClause .= "$joinTableColumn = ?";

                if ($targetClass->containsForeignIdentifier) {
                    $params[] = $targetId[$targetClass->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])];
                } else {
                    $params[] = $targetId[$targetClass->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]];
                }
            } else if (isset($mapping['relationToSourceKeyColumns'][$joinTableColumn])) {
                if ($whereClause !== '') {
                    $whereClause .= ' AND ';
                }
                $whereClause .= "$joinTableColumn = ?";

                if ($sourceClass->containsForeignIdentifier) {
                    $params[] = $sourceId[$sourceClass->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])];
                } else {
                    $params[] = $sourceId[$sourceClass->fieldNames[$mapping['relationToSourceKeyColumns'][$joinTableColumn]]];
                }
            }
        }
        $sql = 'SELECT 1 FROM ' . $joinTable['name'] . ' WHERE ' . $whereClause;

        return (bool)$this->_conn->fetchColumn($sql, $params);
    }
}


text:
<?php
/*
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.doctrine-project.org>.
 */

namespace Doctrine\ORM\Persisters;

use PDO,
    Doctrine\DBAL\LockMode,
    Doctrine\DBAL\Types\Type,
    Doctrine\DBAL\Connection,
    Doctrine\ORM\ORMException,
    Doctrine\ORM\OptimisticLockException,
    Doctrine\ORM\EntityManager,
    Doctrine\ORM\Query,
    Doctrine\ORM\PersistentCollection,
    Doctrine\ORM\Mapping\MappingException,
    Doctrine\ORM\Mapping\ClassMetadata,
    Doctrine\ORM\Events,
    Doctrine\ORM\Event\LifecycleEventArgs;

/**
 * A BasicEntityPersiter maps an entity to a single table in a relational database.
 *
 * A persister is always responsible for a single entity type.
 *
 * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent
 * state of entities onto a relational database when the UnitOfWork is committed,
 * as well as for basic querying of entities and their associations (not DQL).
 *
 * The persisting operations that are invoked during a commit of a UnitOfWork to
 * persist the persistent entity state are:
 *
 *   - {@link addInsert} : To schedule an entity for insertion.
 *   - {@link executeInserts} : To execute all scheduled insertions.
 *   - {@link update} : To update the persistent state of an entity.
 *   - {@link delete} : To delete the persistent state of an entity.
 *
 * As can be seen from the above list, insertions are batched and executed all at once
 * for increased efficiency.
 *
 * The querying operations invoked during a UnitOfWork, either through direct find
 * requests or lazy-loading, are the following:
 *
 *   - {@link load} : Loads (the state of) a single, managed entity.
 *   - {@link loadAll} : Loads multiple, managed entities.
 *   - {@link loadOneToOneEntity} : Loads a one/many-to-one entity association (lazy-loading).
 *   - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading).
 *   - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading).
 *
 * The BasicEntityPersister implementation provides the default behavior for
 * persisting and querying entities that are mapped to a single database table.
 *
 * Subclasses can be created to provide custom persisting and querying strategies,
 * i.e. spanning multiple tables.
 *
 * @author Roman Borschel <roman@code-factory.org>
 * @author Giorgio Sironi <piccoloprincipeazzurro@gmail.com>
 * @author Benjamin Eberlei <kontakt@beberlei.de>
 * @since 2.0
 */
class BasicEntityPersister
{
    /**
     * Metadata object that describes the mapping of the mapped entity class.
     *
     * @var Doctrine\ORM\Mapping\ClassMetadata
     */
    protected $_class;

    /**
     * The underlying DBAL Connection of the used EntityManager.
     *
     * @var Doctrine\DBAL\Connection $conn
     */
    protected $_conn;

    /**
     * The database platform.
     *
     * @var Doctrine\DBAL\Platforms\AbstractPlatform
     */
    protected $_platform;

    /**
     * The EntityManager instance.
     *
     * @var Doctrine\ORM\EntityManager
     */
    protected $_em;

    /**
     * Queued inserts.
     *
     * @var array
     */
    protected $_queuedInserts = array();

    /**
     * ResultSetMapping that is used for all queries. Is generated lazily once per request.
     *
     * TODO: Evaluate Caching in combination with the other cached SQL snippets.
     *
     * @var Query\ResultSetMapping
     */
    protected $_rsm;

    /**
     * The map of column names to DBAL mapping types of all prepared columns used
     * when INSERTing or UPDATEing an entity.
     *
     * @var array
     * @see _prepareInsertData($entity)
     * @see _prepareUpdateData($entity)
     */
    protected $_columnTypes = array();

    /**
     * The INSERT SQL statement used for entities handled by this persister.
     * This SQL is only generated once per request, if at all.
     *
     * @var string
     */
    private $_insertSql;

    /**
     * The SELECT column list SQL fragment used for querying entities by this persister.
     * This SQL fragment is only generated once per request, if at all.
     *
     * @var string
     */
    protected $_selectColumnListSql;

    /**
     * The JOIN SQL fragement used to eagerly load all many-to-one and one-to-one
     * associations configured as FETCH_EAGER, aswell as all inverse one-to-one associations.
     *
     * @var string
     */
    protected $_selectJoinSql;

    /**
     * Counter for creating unique SQL table and column aliases.
     *
     * @var integer
     */
    protected $_sqlAliasCounter = 0;

    /**
     * Map from class names (FQCN) to the corresponding generated SQL table aliases.
     *
     * @var array
     */
    protected $_sqlTableAliases = array();

    /**
     * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
     * and persists instances of the class described by the given ClassMetadata descriptor.
     *
     * @param Doctrine\ORM\EntityManager $em
     * @param Doctrine\ORM\Mapping\ClassMetadata $class
     */
    public function __construct(EntityManager $em, ClassMetadata $class)
    {
        $this->_em = $em;
        $this->_class = $class;
        $this->_conn = $em->getConnection();
        $this->_platform = $this->_conn->getDatabasePlatform();
    }

    /**
     * @return Doctrine\ORM\Mapping\ClassMetadata
     */
    public function getClassMetadata()
    {
        return $this->_class;
    }

    /**
     * Adds an entity to the queued insertions.
     * The entity remains queued until {@link executeInserts} is invoked.
     *
     * @param object $entity The entity to queue for insertion.
     */
    public function addInsert($entity)
    {
        $this->_queuedInserts[spl_object_hash($entity)] = $entity;
    }

    /**
     * Executes all queued entity insertions and returns any generated post-insert
     * identifiers that were created as a result of the insertions.
     *
     * If no inserts are queued, invoking this method is a NOOP.
     *
     * @return array An array of any generated post-insert IDs. This will be an empty array
     *               if the entity class does not use the IDENTITY generation strategy.
     */
    public function executeInserts()
    {
        if ( ! $this->_queuedInserts) {
            return;
        }

        $postInsertIds = array();
        $idGen = $this->_class->idGenerator;
        $isPostInsertId = $idGen->isPostInsertGenerator();

        $stmt = $this->_conn->prepare($this->_getInsertSQL());
        $tableName = $this->_class->table['name'];

        foreach ($this->_queuedInserts as $entity) {
            $insertData = $this->_prepareInsertData($entity);

            if (isset($insertData[$tableName])) {
                $paramIndex = 1;
                foreach ($insertData[$tableName] as $column => $value) {
                    $stmt->bindValue($paramIndex++, $value, $this->_columnTypes[$column]);
                }
            }

            $stmt->execute();

            if ($isPostInsertId) {
                $id = $idGen->generate($this->_em, $entity);
                $postInsertIds[$id] = $entity;
            } else {
                $id = $this->_class->getIdentifierValues($entity);
            }

            if ($this->_class->isVersioned) {
                $this->assignDefaultVersionValue($entity, $id);
            }
        }

        $stmt->closeCursor();
        $this->_queuedInserts = array();

        return $postInsertIds;
    }

    /**
     * Retrieves the default version value which was created
     * by the preceding INSERT statement and assigns it back in to the
     * entities version field.
     *
     * @param object $entity
     * @param mixed $id
     */
    protected function assignDefaultVersionValue($entity, $id)
    {
        $value = $this->fetchVersionValue($this->_class, $id);
        $this->_class->setFieldValue($entity, $this->_class->versionField, $value);
    }

    /**
     * Fetch the current version value of a versioned entity.
     *
     * @param Doctrine\ORM\Mapping\ClassMetadata $versionedClass
     * @param mixed $id
     * @return mixed
     */
    protected function fetchVersionValue($versionedClass, $id)
    {
        $versionField = $versionedClass->versionField;
        $identifier = $versionedClass->getIdentifierColumnNames();
        $versionFieldColumnName = $versionedClass->getColumnName($versionField);
        //FIXME: Order with composite keys might not be correct
        $sql = "SELECT " . $versionFieldColumnName . " FROM " . $versionedClass->getQuotedTableName($this->_platform)
               . " WHERE " . implode(' = ? AND ', $identifier) . " = ?";
        $value = $this->_conn->fetchColumn($sql, array_values((array)$id));

        return Type::getType($versionedClass->fieldMappings[$versionField]['type'])->convertToPHPValue($value, $this->_platform);
    }

    /**
     * Updates a managed entity. The entity is updated according to its current changeset
     * in the running UnitOfWork. If there is no changeset, nothing is updated.
     *
     * The data to update is retrieved through {@link _prepareUpdateData}.
     * Subclasses that override this method are supposed to obtain the update data
     * in the same way, through {@link _prepareUpdateData}.
     *
     * Subclasses are also supposed to take care of versioning when overriding this method,
     * if necessary. The {@link _updateTable} method can be used to apply the data retrieved
     * from {@_prepareUpdateData} on the target tables, thereby optionally applying versioning.
     *
     * @param object $entity The entity to update.
     */
    public function update($entity)
    {
        $updateData = $this->_prepareUpdateData($entity);
        $tableName = $this->_class->table['name'];
        if (isset($updateData[$tableName]) && $updateData[$tableName]) {
            $this->_updateTable(
                $entity, $this->_class->getQuotedTableName($this->_platform),
                $updateData[$tableName], $this->_class->isVersioned
            );

            if ($this->_class->isVersioned) {
                $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
                $this->assignDefaultVersionValue($entity, $id);
            }
        }
    }

    /**
     * Performs an UPDATE statement for an entity on a specific table.
     * The UPDATE can optionally be versioned, which requires the entity to have a version field.
     *
     * @param object $entity The entity object being updated.
     * @param string $quotedTableName The quoted name of the table to apply the UPDATE on.
     * @param array $updateData The map of columns to update (column => value).
     * @param boolean $versioned Whether the UPDATE should be versioned.
     */
    protected final function _updateTable($entity, $quotedTableName, array $updateData, $versioned = false)
    {
        $set = $params = $types = array();

        foreach ($updateData as $columnName => $value) {
            if (isset($this->_class->fieldNames[$columnName])) {
                $set[] = $this->_class->getQuotedColumnName($this->_class->fieldNames[$columnName], $this->_platform) . ' = ?';
            } else {
                $set[] = $columnName . ' = ?';
            }
            $params[] = $value;
            $types[] = $this->_columnTypes[$columnName];
        }

        $where = array();
        $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
        foreach ($this->_class->identifier as $idField) {
            if (isset($this->_class->associationMappings[$idField])) {
                $targetMapping = $this->_em->getClassMetadata($this->_class->associationMappings[$idField]['targetEntity']);
                $where[] = $this->_class->associationMappings[$idField]['joinColumns'][0]['name'];
                $params[] = $id[$idField];
                $types[] = $targetMapping->fieldMappings[$targetMapping->identifier[0]]['type'];
            } else {
                $where[] = $this->_class->getQuotedColumnName($idField, $this->_platform);
                $params[] = $id[$idField];
                $types[] = $this->_class->fieldMappings[$idField]['type'];
            }
        }

        if ($versioned) {
            $versionField = $this->_class->versionField;
            $versionFieldType = $this->_class->fieldMappings[$versionField]['type'];
            $versionColumn = $this->_class->getQuotedColumnName($versionField, $this->_platform);
            if ($versionFieldType == Type::INTEGER) {
                $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1';
            } else if ($versionFieldType == Type::DATETIME) {
                $set[] = $versionColumn . ' = CURRENT_TIMESTAMP';
            }
            $where[] = $versionColumn;
            $params[] = $this->_class->reflFields[$versionField]->getValue($entity);
            $types[] = $this->_class->fieldMappings[$versionField]['type'];
        }

        $sql = "UPDATE $quotedTableName SET " . implode(', ', $set)
            . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';

        $result = $this->_conn->executeUpdate($sql, $params, $types);

        if ($versioned && ! $result) {
            throw OptimisticLockException::lockFailed($entity);
        }
    }

    /**
     * @todo Add check for platform if it supports foreign keys/cascading.
     * @param array $identifier
     * @return void
     */
    protected function deleteJoinTableRecords($identifier)
    {
        foreach ($this->_class->associationMappings as $mapping) {
            if ($mapping['type'] == ClassMetadata::MANY_TO_MANY) {
                // @Todo this only covers scenarios with no inheritance or of the same level. Is there something
                // like self-referential relationship between different levels of an inheritance hierachy? I hope not!
                $selfReferential = ($mapping['targetEntity'] == $mapping['sourceEntity']);

                if ( ! $mapping['isOwningSide']) {
                    $relatedClass = $this->_em->getClassMetadata($mapping['targetEntity']);
                    $mapping = $relatedClass->associationMappings[$mapping['mappedBy']];
                    $keys = array_keys($mapping['relationToTargetKeyColumns']);
                    if ($selfReferential) {
                        $otherKeys = array_keys($mapping['relationToSourceKeyColumns']);
                    }
                } else {
                    $keys = array_keys($mapping['relationToSourceKeyColumns']);
                    if ($selfReferential) {
                        $otherKeys = array_keys($mapping['relationToTargetKeyColumns']);
                    }
                }

                if ( ! isset($mapping['isOnDeleteCascade'])) {
                    $this->_conn->delete($mapping['joinTable']['name'], array_combine($keys, $identifier));

                    if ($selfReferential) {
                        $this->_conn->delete($mapping['joinTable']['name'], array_combine($otherKeys, $identifier));
                    }
                }
            }
        }
    }

    /**
     * Deletes a managed entity.
     *
     * The entity to delete must be managed and have a persistent identifier.
     * The deletion happens instantaneously.
     *
     * Subclasses may override this method to customize the semantics of entity deletion.
     *
     * @param object $entity The entity to delete.
     */
    public function delete($entity)
    {
        $identifier = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
        $this->deleteJoinTableRecords($identifier);

        $id = array_combine($this->_class->getIdentifierColumnNames(), $identifier);
        $this->_conn->delete($this->_class->getQuotedTableName($this->_platform), $id);
    }

    /**
     * Prepares the changeset of an entity for database insertion (UPDATE).
     *
     * The changeset is obtained from the currently running UnitOfWork.
     *
     * During this preparation the array that is passed as the second parameter is filled with
     * <columnName> => <value> pairs, grouped by table name.
     *
     * Example:
     * <code>
     * array(
     *    'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...),
     *    'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...),
     *    ...
     * )
     * </code>
     *
     * @param object $entity The entity for which to prepare the data.
     * @return array The prepared data.
     */
    protected function _prepareUpdateData($entity)
    {
        $result = array();
        $uow = $this->_em->getUnitOfWork();

        if ($versioned = $this->_class->isVersioned) {
            $versionField = $this->_class->versionField;
        }

        foreach ($uow->getEntityChangeSet($entity) as $field => $change) {
            if ($versioned && $versionField == $field) {
                continue;
            }

            $oldVal = $change[0];
            $newVal = $change[1];

            if (isset($this->_class->associationMappings[$field])) {
                $assoc = $this->_class->associationMappings[$field];
                // Only owning side of x-1 associations can have a FK column.
                if ( ! $assoc['isOwningSide'] || ! ($assoc['type'] & ClassMetadata::TO_ONE)) {
                    continue;
                }

                if ($newVal !== null) {
                    $oid = spl_object_hash($newVal);
                    if (isset($this->_queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) {
                        // The associated entity $newVal is not yet persisted, so we must
                        // set $newVal = null, in order to insert a null value and schedule an
                        // extra update on the UnitOfWork.
                        $uow->scheduleExtraUpdate($entity, array(
                            $field => array(null, $newVal)
                        ));
                        $newVal = null;
                    }
                }

                if ($newVal !== null) {
                    $newValId = $uow->getEntityIdentifier($newVal);
                }

                $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']);
                $owningTable = $this->getOwningTable($field);

                foreach ($assoc['sourceToTargetKeyColumns'] as $sourceColumn => $targetColumn) {
                    if ($newVal === null) {
                        $result[$owningTable][$sourceColumn] = null;
                    } else if ($targetClass->containsForeignIdentifier) {
                        $result[$owningTable][$sourceColumn] = $newValId[$targetClass->getFieldForColumn($targetColumn)];
                    } else {
                        $result[$owningTable][$sourceColumn] = $newValId[$targetClass->fieldNames[$targetColumn]];
                    }
                    $this->_columnTypes[$sourceColumn] = $targetClass->getTypeOfColumn($targetColumn);
                }
            } else {
                $columnName = $this->_class->columnNames[$field];
                $this->_columnTypes[$columnName] = $this->_class->fieldMappings[$field]['type'];
                $result[$this->getOwningTable($field)][$columnName] = $newVal;
            }
        }
        return $result;
    }

    /**
     * Prepares the data changeset of a managed entity for database insertion (initial INSERT).
     * The changeset of the entity is obtained from the currently running UnitOfWork.
     *
     * The default insert data preparation is the same as for updates.
     *
     * @param object $entity The entity for which to prepare the data.
     * @return array The prepared data for the tables to update.
     * @see _prepareUpdateData
     */
    protected function _prepareInsertData($entity)
    {
        return $this->_prepareUpdateData($entity);
    }

    /**
     * Gets the name of the table that owns the column the given field is mapped to.
     *
     * The default implementation in BasicEntityPersister always returns the name
     * of the table the entity type of this persister is mapped to, since an entity
     * is always persisted to a single table with a BasicEntityPersister.
     *
     * @param string $fieldName The field name.
     * @return string The table name.
     */
    public function getOwningTable($fieldName)
    {
        return $this->_class->table['name'];
    }

    /**
     * Loads an entity by a list of field criteria.
     *
     * @param array $criteria The criteria by which to load the entity.
     * @param object $entity The entity to load the data into. If not specified,
     *        a new entity is created.
     * @param $assoc The association that connects the entity to load to another entity, if any.
     * @param array $hints Hints for entity creation.
     * @param int $lockMode
     * @return object The loaded and managed entity instance or NULL if the entity can not be found.
     * @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
     */
    public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0)
    {
        $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, $lockMode);
        list($params, $types) = $this->expandParameters($criteria);
        $stmt = $this->_conn->executeQuery($sql, $params, $types);

        if ($entity !== null) {
            $hints[Query::HINT_REFRESH] = true;
            $hints[Query::HINT_REFRESH_ENTITY] = $entity;
        }

        if ($this->_selectJoinSql) {
            $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT);
        } else {
            $hydrator = $this->_em->newHydrator(Query::HYDRATE_SIMPLEOBJECT);
        }
        $entities = $hydrator->hydrateAll($stmt, $this->_rsm, $hints);
        return $entities ? $entities[0] : null;
    }

    /**
     * Loads an entity of this persister's mapped class as part of a single-valued
     * association from another entity.
     *
     * @param array $assoc The association to load.
     * @param object $sourceEntity The entity that owns the association (not necessarily the "owning side").
     * @param array $identifier The identifier of the entity to load. Must be provided if
     *                          the association to load represents the owning side, otherwise
     *                          the identifier is derived from the $sourceEntity.
     * @return object The loaded and managed entity instance or NULL if the entity can not be found.
     */
    public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = array())
    {
        if ($foundEntity = $this->_em->getUnitOfWork()->tryGetById($identifier, $assoc['targetEntity'])) {
            return $foundEntity;
        }

        $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']);

        if ($assoc['isOwningSide']) {
            $isInverseSingleValued = $assoc['inversedBy'] && ! $targetClass->isCollectionValuedAssociation($assoc['inversedBy']);

            // Mark inverse side as fetched in the hints, otherwise the UoW would
            // try to load it in a separate query (remember: to-one inverse sides can not be lazy).
            $hints = array();
            if ($isInverseSingleValued) {
                $hints['fetched'][$targetClass->name][$assoc['inversedBy']] = true;
                if ($targetClass->subClasses) {
                    foreach ($targetClass->subClasses as $targetSubclassName) {
                        $hints['fetched'][$targetSubclassName][$assoc['inversedBy']] = true;
                    }
                }
            }
            /* cascade read-only status
            if ($this->_em->getUnitOfWork()->isReadOnly($sourceEntity)) {
                $hints[Query::HINT_READ_ONLY] = true;
            }
            */

            $targetEntity = $this->load($identifier, null, $assoc, $hints);

            // Complete bidirectional association, if necessary
            if ($targetEntity !== null && $isInverseSingleValued) {
                $targetClass->reflFields[$assoc['inversedBy']]->setValue($targetEntity, $sourceEntity);
            }
        } else {
            $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']);
            $owningAssoc = $targetClass->getAssociationMapping($assoc['mappedBy']);
            // TRICKY: since the association is specular source and target are flipped
            foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
                if (isset($sourceClass->fieldNames[$sourceKeyColumn])) {
                    // unset the old value and set the new sql aliased value here. By definition
                    // unset($identifier[$targetKeyColumn] works here with how UnitOfWork::createEntity() calls this method.
                    $identifier[$this->_getSQLTableAlias($targetClass->name) . "." . $targetKeyColumn] =
                        $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
                    unset($identifier[$targetKeyColumn]);
                } else {
                    throw MappingException::joinColumnMustPointToMappedField(
                        $sourceClass->name, $sourceKeyColumn
                    );
                }
            }

            $targetEntity = $this->load($identifier, null, $assoc);

            if ($targetEntity !== null) {
                $targetClass->setFieldValue($targetEntity, $assoc['mappedBy'], $sourceEntity);
            }
        }

        return $targetEntity;
    }

    /**
     * Refreshes a managed entity.
     *
     * @param array $id The identifier of the entity as an associative array from
     *                  column or field names to values.
     * @param object $entity The entity to refresh.
     */
    public function refresh(array $id, $entity)
    {
        $sql = $this->_getSelectEntitiesSQL($id);
        list($params, $types) = $this->expandParameters($id);
        $stmt = $this->_conn->executeQuery($sql, $params, $types);

        $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT);
        $hydrator->hydrateAll($stmt, $this->_rsm, array(Query::HINT_REFRESH => true));

        if (isset($this->_class->lifecycleCallbacks[Events::postLoad])) {
            $this->_class->invokeLifecycleCallbacks(Events::postLoad, $entity);
        }
        $evm = $this->_em->getEventManager();
        if ($evm->hasListeners(Events::postLoad)) {
            $evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity, $this->_em));
        }
    }

    /**
     * Loads a list of entities by a list of field criteria.
     *
     * @param array $criteria
     * @param array $orderBy
     * @param int $limit
     * @param int $offset
     * @return array
     */
    public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null)
    {
        $entities = array();
        $sql = $this->_getSelectEntitiesSQL($criteria, null, 0, $limit, $offset, $orderBy);
        list($params, $types) = $this->expandParameters($criteria);
        $stmt = $this->_conn->executeQuery($sql, $params, $types);

        if ($this->_selectJoinSql) {
            $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT);
        } else {
            $hydrator = $this->_em->newHydrator(Query::HYDRATE_SIMPLEOBJECT);
        }
        return $hydrator->hydrateAll($stmt, $this->_rsm, array('deferEagerLoads' => true));
    }

    /**
     * Get (sliced or full) elements of the given collection.
     *
     * @param array $assoc
     * @param object $sourceEntity
     * @param int|null $offset
     * @param int|null $limit
     * @return array
     */
    public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
    {
        $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit);
        return $this->loadArrayFromStatement($assoc, $stmt);
    }

    /**
     * Load an array of entities from a given dbal statement.
     *
     * @param array $assoc
     * @param Doctrine\DBAL\Statement $stmt
     * @return array
     */
    private function loadArrayFromStatement($assoc, $stmt)
    {
        $hints = array('deferEagerLoads' => true);

        if (isset($assoc['indexBy'])) {
            $rsm = clone ($this->_rsm); // this is necessary because the "default rsm" should be changed.
            $rsm->addIndexBy('r', $assoc['indexBy']);
        } else {
            $rsm = $this->_rsm;
        }

        $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT);
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
    }

    /**
     * Hydrate a collection from a given dbal statement.
     *
     * @param array $assoc
     * @param Doctrine\DBAL\Statement $stmt
     * @param PersistentCollection $coll
     */
    private function loadCollectionFromStatement($assoc, $stmt, $coll)
    {
        $hints = array('deferEagerLoads' => true, 'collection' => $coll);

        if (isset($assoc['indexBy'])) {
            $rsm = clone ($this->_rsm); // this is necessary because the "default rsm" should be changed.
            $rsm->addIndexBy('r', $assoc['indexBy']);
        } else {
            $rsm = $this->_rsm;
        }

        $hydrator = $this->_em->newHydrator(Query::HYDRATE_OBJECT);
        $hydrator->hydrateAll($stmt, $rsm, $hints);
    }

    /**
     * Loads a collection of entities of a many-to-many association.
     *
     * @param ManyToManyMapping $assoc The association mapping of the association being loaded.
     * @param object $sourceEntity The entity that owns the collection.
     * @param PersistentCollection $coll The collection to fill.
     * @param int|null $offset
     * @param int|null $limit
     * @return array
     */
    public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
    {
        $stmt = $this->getManyToManyStatement($assoc, $sourceEntity);
        return $this->loadCollectionFromStatement($assoc, $stmt, $coll);
    }

    private function getManyToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
    {
        $criteria = array();
        $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']);
        if ($assoc['isOwningSide']) {
            $quotedJoinTable = $sourceClass->getQuotedJoinTableName($assoc, $this->_platform);
            foreach ($assoc['relationToSourceKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) {
                if ($sourceClass->containsForeignIdentifier) {
                    $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
                    $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
                    if (isset($sourceClass->associationMappings[$field])) {
                        $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value);
                        $value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]];
                    }

                    $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $value;
                } else if (isset($sourceClass->fieldNames[$sourceKeyColumn])) {
                    $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
                } else {
                    throw MappingException::joinColumnMustPointToMappedField(
                        $sourceClass->name, $sourceKeyColumn
                    );
                }
            }
        } else {
            $owningAssoc = $this->_em->getClassMetadata($assoc['targetEntity'])->associationMappings[$assoc['mappedBy']];
            $quotedJoinTable = $sourceClass->getQuotedJoinTableName($owningAssoc, $this->_platform);
            // TRICKY: since the association is inverted source and target are flipped
            foreach ($owningAssoc['relationToTargetKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) {
                if ($sourceClass->containsForeignIdentifier) {
                    $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
                    $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
                    if (isset($sourceClass->associationMappings[$field])) {
                        $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value);
                        $value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]];
                    }
                    $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $value;
                } else if (isset($sourceClass->fieldNames[$sourceKeyColumn])) {
                    $criteria[$quotedJoinTable . "." . $relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
                } else {
                    throw MappingException::joinColumnMustPointToMappedField(
                        $sourceClass->name, $sourceKeyColumn
                    );
                }
            }
        }

        $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset);
        list($params, $types) = $this->expandParameters($criteria);
        return $this->_conn->executeQuery($sql, $params, $types);
    }

    /**
     * Gets the SELECT SQL to select one or more entities by a set of field criteria.
     *
     * @param array $criteria
     * @param AssociationMapping $assoc
     * @param string $orderBy
     * @param int $lockMode
     * @param int $limit
     * @param int $offset
     * @param array $orderBy
     * @return string
     * @todo Refactor: _getSelectSQL(...)
     */
    protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null)
    {
        $joinSql = $assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY ?
                $this->_getSelectManyToManyJoinSQL($assoc) : '';

        $conditionSql = $this->_getSelectConditionSQL($criteria, $assoc);

        $orderBy = ($assoc !== null && isset($assoc['orderBy'])) ? $assoc['orderBy'] : $orderBy;
        $orderBySql = $orderBy ? $this->_getOrderBySQL($orderBy, $this->_getSQLTableAlias($this->_class->name)) : '';

        $lockSql = '';
        if ($lockMode == LockMode::PESSIMISTIC_READ) {
            $lockSql = ' ' . $this->_platform->getReadLockSql();
        } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) {
            $lockSql = ' ' . $this->_platform->getWriteLockSql();
        }

        return $this->_platform->modifyLimitQuery('SELECT ' . $this->_getSelectColumnListSQL()
             . $this->_platform->appendLockHint(' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' '
             . $this->_getSQLTableAlias($this->_class->name), $lockMode)
             . $this->_selectJoinSql . $joinSql
             . ($conditionSql ? ' WHERE ' . $conditionSql : '')
             . $orderBySql, $limit, $offset)
             . $lockSql;
    }

    /**
     * Gets the ORDER BY SQL snippet for ordered collections.
     *
     * @param array $orderBy
     * @param string $baseTableAlias
     * @return string
     * @todo Rename: _getOrderBySQL
     */
    protected final function _getOrderBySQL(array $orderBy, $baseTableAlias)
    {
        $orderBySql = '';
        foreach ($orderBy as $fieldName => $orientation) {
            if ( ! isset($this->_class->fieldMappings[$fieldName])) {
                throw ORMException::unrecognizedField($fieldName);
            }

            $tableAlias = isset($this->_class->fieldMappings[$fieldName]['inherited']) ?
                    $this->_getSQLTableAlias($this->_class->fieldMappings[$fieldName]['inherited'])
                    : $baseTableAlias;

            $columnName = $this->_class->getQuotedColumnName($fieldName, $this->_platform);
            $orderBySql .= $orderBySql ? ', ' : ' ORDER BY ';
            $orderBySql .= $tableAlias . '.' . $columnName . ' ' . $orientation;
        }

        return $orderBySql;
    }

    /**
     * Gets the SQL fragment with the list of columns to select when querying for
     * an entity in this persister.
     *
     * Subclasses should override this method to alter or change the select column
     * list SQL fragment. Note that in the implementation of BasicEntityPersister
     * the resulting SQL fragment is generated only once and cached in {@link _selectColumnListSql}.
     * Subclasses may or may not do the same.
     *
     * @return string The SQL fragment.
     * @todo Rename: _getSelectColumnsSQL()
     */
    protected function _getSelectColumnListSQL()
    {
        if ($this->_selectColumnListSql !== null) {
            return $this->_selectColumnListSql;
        }

        $columnList = '';
        $this->_rsm = new Query\ResultSetMapping();
        $this->_rsm->addEntityResult($this->_class->name, 'r'); // r for root

        // Add regular columns to select list
        foreach ($this->_class->fieldNames as $field) {
            if ($columnList) $columnList .= ', ';
            $columnList .= $this->_getSelectColumnSQL($field, $this->_class);
        }

        $this->_selectJoinSql = '';
        $eagerAliasCounter = 0;
        foreach ($this->_class->associationMappings as $assocField => $assoc) {
            $assocColumnSQL = $this->_getSelectColumnAssociationSQL($assocField, $assoc, $this->_class);
            if ($assocColumnSQL) {
                if ($columnList) $columnList .= ', ';
                $columnList .= $assocColumnSQL;
            }

            if ($assoc['type'] & ClassMetadata::TO_ONE && ($assoc['fetch'] == ClassMetadata::FETCH_EAGER || !$assoc['isOwningSide'])) {
                $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']);
                if ($eagerEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) {
                    continue; // now this is why you shouldn't use inheritance
                }

                $assocAlias = 'e' . ($eagerAliasCounter++);
                $this->_rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField);

                foreach ($eagerEntity->fieldNames AS $field) {
                    if ($columnList) $columnList .= ', ';
                    $columnList .= $this->_getSelectColumnSQL($field, $eagerEntity, $assocAlias);
                }

                foreach ($eagerEntity->associationMappings as $assoc2Field => $assoc2) {
                    $assoc2ColumnSQL = $this->_getSelectColumnAssociationSQL($assoc2Field, $assoc2, $eagerEntity, $assocAlias);
                    if ($assoc2ColumnSQL) {
                        if ($columnList) $columnList .= ', ';
                        $columnList .= $assoc2ColumnSQL;
                    }
                }
                $this->_selectJoinSql .= ' LEFT JOIN'; // TODO: Inner join when all join columns are NOT nullable.
                $first = true;
                if ($assoc['isOwningSide']) {
                    $this->_selectJoinSql .= ' ' . $eagerEntity->table['name'] . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON ';

                    foreach ($assoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) {
                        if (!$first) {
                            $this->_selectJoinSql .= ' AND ';
                        }
                        $this->_selectJoinSql .= $this->_getSQLTableAlias($assoc['sourceEntity']) . '.'.$sourceCol.' = ' .
                                                 $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias) . '.'.$targetCol.' ';
                        $first = false;
                    }
                } else {
                    $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']);
                    $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']);

                    $this->_selectJoinSql .= ' ' . $eagerEntity->table['name'] . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON ';

                    foreach ($owningAssoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) {
                        if (!$first) {
                            $this->_selectJoinSql .= ' AND ';
                        }
                        $this->_selectJoinSql .= $this->_getSQLTableAlias($owningAssoc['sourceEntity'], $assocAlias) . '.'.$sourceCol.' = ' .
                                                 $this->_getSQLTableAlias($owningAssoc['targetEntity']) . '.' . $targetCol . ' ';
                        $first = false;
                    }
                }
            }
        }

        $this->_selectColumnListSql = $columnList;

        return $this->_selectColumnListSql;
    }

    protected function _getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r')
    {
        $columnList = '';
        if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
            foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) {
                if ($columnList) $columnList .= ', ';

                $columnAlias = $srcColumn . $this->_sqlAliasCounter++;
                $columnList .= $this->_getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) )  . ".$srcColumn AS $columnAlias";
                $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias);
                $this->_rsm->addMetaResult($alias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn, isset($assoc['id']) && $assoc['id'] === true);
            }
        }
        return $columnList;
    }

    /**
     * Gets the SQL join fragment used when selecting entities from a
     * many-to-many association.
     *
     * @param ManyToManyMapping $manyToMany
     * @return string
     */
    protected function _getSelectManyToManyJoinSQL(array $manyToMany)
    {
        if ($manyToMany['isOwningSide']) {
            $owningAssoc = $manyToMany;
            $joinClauses = $manyToMany['relationToTargetKeyColumns'];
        } else {
            $owningAssoc = $this->_em->getClassMetadata($manyToMany['targetEntity'])->associationMappings[$manyToMany['mappedBy']];
            $joinClauses = $owningAssoc['relationToSourceKeyColumns'];
        }

        $joinTableName = $this->_class->getQuotedJoinTableName($owningAssoc, $this->_platform);

        $joinSql = '';
        foreach ($joinClauses as $joinTableColumn => $sourceColumn) {
            if ($joinSql != '') $joinSql .= ' AND ';

            if ($this->_class->containsForeignIdentifier && !isset($this->_class->fieldNames[$sourceColumn])) {
                $quotedColumn = $sourceColumn; // join columns cannot be quoted
            } else {
                $quotedColumn = $this->_class->getQuotedColumnName($this->_class->fieldNames[$sourceColumn], $this->_platform);
            }

            $joinSql .= $this->_getSQLTableAlias($this->_class->name) .
                    '.' . $quotedColumn . ' = '
                    . $joinTableName . '.' . $joinTableColumn;
        }

        return " INNER JOIN $joinTableName ON $joinSql";
    }

    /**
     * Gets the INSERT SQL used by the persister to persist a new entity.
     *
     * @return string
     */
    protected function _getInsertSQL()
    {
        if ($this->_insertSql === null) {
            $insertSql = '';
            $columns = $this->_getInsertColumnList();
            if (empty($columns)) {
                $insertSql = $this->_platform->getEmptyIdentityInsertSQL(
                        $this->_class->getQuotedTableName($this->_platform),
                        $this->_class->getQuotedColumnName($this->_class->identifier[0], $this->_platform)
                );
            } else {
                $columns = array_unique($columns);
                $values = array_fill(0, count($columns), '?');

                $insertSql = 'INSERT INTO ' . $this->_class->getQuotedTableName($this->_platform)
                        . ' (' . implode(', ', $columns) . ') '
                        . 'VALUES (' . implode(', ', $values) . ')';
            }
            $this->_insertSql = $insertSql;
        }
        return $this->_insertSql;
    }

    /**
     * Gets the list of columns to put in the INSERT SQL statement.
     *
     * Subclasses should override this method to alter or change the list of
     * columns placed in the INSERT statements used by the persister.
     *
     * @return array The list of columns.
     */
    protected function _getInsertColumnList()
    {
        $columns = array();
        foreach ($this->_class->reflFields as $name => $field) {
            if ($this->_class->isVersioned && $this->_class->versionField == $name) {
                continue;
            }
            if (isset($this->_class->associationMappings[$name])) {
                $assoc = $this->_class->associationMappings[$name];
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
                    foreach ($assoc['targetToSourceKeyColumns'] as $sourceCol) {
                        $columns[] = $sourceCol;
                    }
                }
            } else if ($this->_class->generatorType != ClassMetadata::GENERATOR_TYPE_IDENTITY ||
                    $this->_class->identifier[0] != $name) {
                $columns[] = $this->_class->getQuotedColumnName($name, $this->_platform);
            }
        }

        return $columns;
    }

    /**
     * Gets the SQL snippet of a qualified column name for the given field name.
     *
     * @param string $field The field name.
     * @param ClassMetadata $class The class that declares this field. The table this class is
     *                             mapped to must own the column for the given field.
     * @param string $alias
     */
    protected function _getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
    {
        $columnName = $class->columnNames[$field];
        $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias) . '.' . $class->getQuotedColumnName($field, $this->_platform);
        $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++);
        $this->_rsm->addFieldResult($alias, $columnAlias, $field);

        return "$sql AS $columnAlias";
    }

    /**
     * Gets the SQL table alias for the given class name.
     *
     * @param string $className
     * @return string The SQL table alias.
     * @todo Reconsider. Binding table aliases to class names is not such a good idea.
     */
    protected function _getSQLTableAlias($className, $assocName = '')
    {
        if ($assocName) {
            $className .= '#'.$assocName;
        }

        if (isset($this->_sqlTableAliases[$className])) {
            return $this->_sqlTableAliases[$className];
        }
        $tableAlias = 't' . $this->_sqlAliasCounter++;

        $this->_sqlTableAliases[$className] = $tableAlias;
        return $tableAlias;
    }

    /**
     * Lock all rows of this entity matching the given criteria with the specified pessimistic lock mode
     *
     * @param array $criteria
     * @param int $lockMode
     * @return void
     */
    public function lock(array $criteria, $lockMode)
    {
        $conditionSql = $this->_getSelectConditionSQL($criteria);

        if ($lockMode == LockMode::PESSIMISTIC_READ) {
            $lockSql = $this->_platform->getReadLockSql();
        } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) {
            $lockSql = $this->_platform->getWriteLockSql();
        }

        $sql = 'SELECT 1 '
             . $this->_platform->appendLockHint($this->getLockTablesSql(), $lockMode)
             . ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ' . $lockSql;
        list($params, $types) = $this->expandParameters($criteria);
        $stmt = $this->_conn->executeQuery($sql, $params, $types);
    }

    /**
     * Get the FROM and optionally JOIN conditions to lock the entity managed by this persister.
     *
     * @return string
     */
    protected function getLockTablesSql()
    {
        return 'FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' '
                . $this->_getSQLTableAlias($this->_class->name);
    }

    /**
     * Gets the conditional SQL fragment used in the WHERE clause when selecting
     * entities in this persister.
     *
     * Subclasses are supposed to override this method if they intend to change
     * or alter the criteria by which entities are selected.
     *
     * @param array $criteria
     * @param AssociationMapping $assoc
     * @return string
     */
    protected function _getSelectConditionSQL(array $criteria, $assoc = null)
    {
        $conditionSql = '';
        foreach ($criteria as $field => $value) {
            $conditionSql .= $conditionSql ? ' AND ' : '';

            if (isset($this->_class->columnNames[$field])) {
                if (isset($this->_class->fieldMappings[$field]['inherited'])) {
                    $conditionSql .= $this->_getSQLTableAlias($this->_class->fieldMappings[$field]['inherited']) . '.';
                } else {
                    $conditionSql .= $this->_getSQLTableAlias($this->_class->name) . '.';
                }
                $conditionSql .= $this->_class->getQuotedColumnName($field, $this->_platform);
            } else if (isset($this->_class->associationMappings[$field])) {
                if (!$this->_class->associationMappings[$field]['isOwningSide']) {
                    throw ORMException::invalidFindByInverseAssociation($this->_class->name, $field);
                }

                if (isset($this->_class->associationMappings[$field]['inherited'])) {
                    $conditionSql .= $this->_getSQLTableAlias($this->_class->associationMappings[$field]['inherited']) . '.';
                } else {
                    $conditionSql .= $this->_getSQLTableAlias($this->_class->name) . '.';
                }

                $conditionSql .= $this->_class->associationMappings[$field]['joinColumns'][0]['name'];
            } else if ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false) {
                // very careless developers could potentially open up this normally hidden api for userland attacks,
                // therefore checking for spaces and function calls which are not allowed.

                // found a join column condition, not really a "field"
                $conditionSql .= $field;
            } else {
                throw ORMException::unrecognizedField($field);
            }
            $conditionSql .= (is_array($value)) ? ' IN (?)' : (($value === null) ? ' IS NULL' : ' = ?');
        }
        return $conditionSql;
    }

    /**
     * Return an array with (sliced or full list) of elements in the specified collection.
     *
     * @param array $assoc
     * @param object $sourceEntity
     * @param int $offset
     * @param int $limit
     * @return array
     */
    public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
    {
        $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit);
        return $this->loadArrayFromStatement($assoc, $stmt);
    }

    /**
     * Loads a collection of entities in a one-to-many association.
     *
     * @param array $assoc
     * @param object $sourceEntity
     * @param PersistentCollection $coll The collection to load/fill.
     * @param int|null $offset
     * @param int|null $limit
     */
    public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
    {
        $stmt = $this->getOneToManyStatement($assoc, $sourceEntity);
        $this->loadCollectionFromStatement($assoc, $stmt, $coll);
    }

    /**
     * Build criteria and execute SQL statement to fetch the one to many entities from.
     *
     * @param array $assoc
     * @param object $sourceEntity
     * @param int|null $offset
     * @param int|null $limit
     * @return Doctrine\DBAL\Statement
     */
    private function getOneToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
    {
        $criteria = array();
        $owningAssoc = $this->_class->associationMappings[$assoc['mappedBy']];
        $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']);

        $tableAlias = isset($owningAssoc['inherited']) ?
                    $this->_getSQLTableAlias($owningAssoc['inherited'])
                    : $this->_getSQLTableAlias($this->_class->name);

        foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
            if ($sourceClass->containsForeignIdentifier) {
                $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
                $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
                if (isset($sourceClass->associationMappings[$field])) {
                    $value = $this->_em->getUnitOfWork()->getEntityIdentifier($value);
                    $value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]];
                }
                $criteria[$tableAlias . "." . $targetKeyColumn] = $value;
            } else {
                $criteria[$tableAlias . "." . $targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
            }
        }

        $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, 0, $limit, $offset);
        list($params, $types) = $this->expandParameters($criteria);

        return $this->_conn->executeQuery($sql, $params, $types);
    }

    /**
     * Expand the parameters from the given criteria and use the correct binding types if found.
     *
     * @param  array $criteria
     * @return array
     */
    private function expandParameters($criteria)
    {
        $params = $types = array();

        foreach ($criteria AS $field => $value) {
            if ($value === null) {
                continue; // skip null values.
            }

            $type = null;
            if (isset($this->_class->fieldMappings[$field])) {
                $type = Type::getType($this->_class->fieldMappings[$field]['type'])->getBindingType();
            }
            if (is_array($value)) {
                $type += Connection::ARRAY_PARAM_OFFSET;
            }

            $params[] = $value;
            $types[] = $type;
        }
        return array($params, $types);
    }

    /**
     * Checks whether the given managed entity exists in the database.
     *
     * @param object $entity
     * @return boolean TRUE if the entity exists in the database, FALSE otherwise.
     */
    public function exists($entity, array $extraConditions = array())
    {
        $criteria = $this->_class->getIdentifierValues($entity);
        if ($extraConditions) {
            $criteria = array_merge($criteria, $extraConditions);
        }

        $sql = 'SELECT 1 FROM ' . $this->_class->getQuotedTableName($this->_platform)
                . ' ' . $this->_getSQLTableAlias($this->_class->name)
                . ' WHERE ' . $this->_getSelectConditionSQL($criteria);

        return (bool) $this->_conn->fetchColumn($sql, array_values($criteria));
    }
}

repo_name: michael-romer/zf-boilerplate

repo_language: PHP

mime_type: text/html


text:
<?php

namespace Doctrine\ORM\Persisters;

class UnionSubclassPersister extends BasicEntityPersister
{

}


text:
<?php
/*
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.doctrine-project.org>.
 */

namespace Doctrine\ORM\Persisters;

use Doctrine\ORM\Mapping\ClassMetadata;

/**
 * Persister for entities that participate in a hierarchy mapped with the
 * SINGLE_TABLE strategy.
 *
 * @author Roman Borschel <roman@code-factory.org>
 * @author Benjamin Eberlei <kontakt@beberlei.de>
 * @since 2.0
 * @link http://martinfowler.com/eaaCatalog/singleTableInheritance.html
 */
class SingleTablePersister extends AbstractEntityInheritancePersister
{
    /** {@inheritdoc} */
    protected function _getDiscriminatorColumnTableName()
    {
        return $this->_class->table['name'];
    }

    /** {@inheritdoc} */
    protected function _getSelectColumnListSQL()
    {
        $columnList = parent::_getSelectColumnListSQL();

        // Append discriminator column
        $discrColumn = $this->_class->discriminatorColumn['name'];
        $columnList .= ", $discrColumn";
        $rootClass = $this->_em->getClassMetadata($this->_class->rootEntityName);
        $tableAlias = $this->_getSQLTableAlias($rootClass->name);
        $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn);
        $this->_rsm->setDiscriminatorColumn('r', $resultColumnName);
        $this->_rsm->addMetaResult('r', $resultColumnName, $discrColumn);

        // Append subclass columns
        foreach ($this->_class->subClasses as $subClassName) {
            $subClass = $this->_em->getClassMetadata($subClassName);
            // Regular columns
            foreach ($subClass->fieldMappings as $fieldName => $mapping) {
                if ( ! isset($mapping['inherited'])) {
                    $columnList .= ', ' . $this->_getSelectColumnSQL($fieldName, $subClass);
                }
            }
            // Foreign key columns
            foreach ($subClass->associationMappings as $assoc) {
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE && ! isset($assoc['inherited'])) {
                    foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) {
                        if ($columnList != '') $columnList .= ', ';
                        $columnList .= $this->getSelectJoinColumnSQL($tableAlias, $srcColumn,
                            isset($assoc['inherited']) ? $assoc['inherited'] : $this->_class->name
                        );
                    }
                }
            }
        }

        return $columnList;
    }

    /** {@inheritdoc} */
    protected function _getInsertColumnList()
    {
        $columns = parent::_getInsertColumnList();
        // Add discriminator column to the INSERT SQL
        $columns[] = $this->_class->discriminatorColumn['name'];

        return $columns;
    }

    /** {@inheritdoc} */
    protected function _getSQLTableAlias($className, $assocName = '')
    {
        return parent::_getSQLTableAlias($this->_class->rootEntityName, $assocName);
    }

    /** {@inheritdoc} */
    protected function _getSelectConditionSQL(array $criteria, $assoc = null)
    {
        $conditionSql = parent::_getSelectConditionSQL($criteria, $assoc);

        // Append discriminator condition
        if ($conditionSql) $conditionSql .= ' AND ';
        $values = array();
        if ($this->_class->discriminatorValue !== null) { // discriminators can be 0
            $values[] = $this->_conn->quote($this->_class->discriminatorValue);
        }

        $discrValues = array_flip($this->_class->discriminatorMap);
        foreach ($this->_class->subClasses as $subclassName) {
            $values[] = $this->_conn->quote($discrValues[$subclassName]);
        }
        $conditionSql .= $this->_getSQLTableAlias($this->_class->name) . '.'
                . $this->_class->discriminatorColumn['name']
                . ' IN (' . implode(', ', $values) . ')';

        return $conditionSql;
    }
}


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

namespace Doctrine\ORM\Persisters;

/**
 * Persister for collections of basic elements / value types.
 *
 * @author robo
 * @todo Implementation once support for collections of basic elements (i.e. strings) is added.
 */
abstract class ElementCollectionPersister extends AbstractCollectionPersister
{
    //put your code here
}


text:
<?php
/*
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.doctrine-project.org>.
 */

namespace Doctrine\ORM\Persisters;

use Doctrine\ORM\ORMException,
    Doctrine\ORM\Mapping\ClassMetadata,
    Doctrine\DBAL\LockMode,
    Doctrine\ORM\Query\ResultSetMapping;

/**
 * The joined subclass persister maps a single entity instance to several tables in the
 * database as it is defined by the <tt>Class Table Inheritance</tt> strategy.
 *
 * @author Roman Borschel <roman@code-factory.org>
 * @author Benjamin Eberlei <kontakt@beberlei.de>
 * @since 2.0
 * @see http://martinfowler.com/eaaCatalog/classTableInheritance.html
 */
class JoinedSubclassPersister extends AbstractEntityInheritancePersister
{
    /**
     * Map that maps column names to the table names that own them.
     * This is mainly a temporary cache, used during a single request.
     *
     * @var array
     */
    private $_owningTableMap = array();

    /**
     * Map of table to quoted table names.
     *
     * @var array
     */
    private $_quotedTableMap = array();

    /**
     * {@inheritdoc}
     */
    protected function _getDiscriminatorColumnTableName()
    {
        if ($this->_class->name == $this->_class->rootEntityName) {
            return $this->_class->table['name'];
        } else {
            return $this->_em->getClassMetadata($this->_class->rootEntityName)->table['name'];
        }
    }

    /**
     * This function finds the ClassMetadata instance in an inheritance hierarchy
     * that is responsible for enabling versioning.
     *
     * @return Doctrine\ORM\Mapping\ClassMetadata
     */
    private function _getVersionedClassMetadata()
    {
        if (isset($this->_class->fieldMappings[$this->_class->versionField]['inherited'])) {
            $definingClassName = $this->_class->fieldMappings[$this->_class->versionField]['inherited'];
            return $this->_em->getClassMetadata($definingClassName);
        }
        return $this->_class;
    }

    /**
     * Gets the name of the table that owns the column the given field is mapped to.
     *
     * @param string $fieldName
     * @return string
     * @override
     */
    public function getOwningTable($fieldName)
    {
        if (!isset($this->_owningTableMap[$fieldName])) {
            if (isset($this->_class->associationMappings[$fieldName]['inherited'])) {
                $cm = $this->_em->getClassMetadata($this->_class->associationMappings[$fieldName]['inherited']);
            } else if (isset($this->_class->fieldMappings[$fieldName]['inherited'])) {
                $cm = $this->_em->getClassMetadata($this->_class->fieldMappings[$fieldName]['inherited']);
            } else {
                $cm = $this->_class;
            }
            $this->_owningTableMap[$fieldName] = $cm->table['name'];
            $this->_quotedTableMap[$cm->table['name']] = $cm->getQuotedTableName($this->_platform);
        }

        return $this->_owningTableMap[$fieldName];
    }

    /**
     * {@inheritdoc}
     */
    public function executeInserts()
    {
        if ( ! $this->_queuedInserts) {
            return;
        }

        $postInsertIds = array();
        $idGen = $this->_class->idGenerator;
        $isPostInsertId = $idGen->isPostInsertGenerator();

        // Prepare statement for the root table
        $rootClass = $this->_class->name == $this->_class->rootEntityName ?
                $this->_class : $this->_em->getClassMetadata($this->_class->rootEntityName);
        $rootPersister = $this->_em->getUnitOfWork()->getEntityPersister($rootClass->name);
        $rootTableName = $rootClass->table['name'];
        $rootTableStmt = $this->_conn->prepare($rootPersister->_getInsertSQL());

        // Prepare statements for sub tables.
        $subTableStmts = array();
        if ($rootClass !== $this->_class) {
            $subTableStmts[$this->_class->table['name']] = $this->_conn->prepare($this->_getInsertSQL());
        }
        foreach ($this->_class->parentClasses as $parentClassName) {
            $parentClass = $this->_em->getClassMetadata($parentClassName);
            $parentTableName = $parentClass->table['name'];
            if ($parentClass !== $rootClass) {
                $parentPersister = $this->_em->getUnitOfWork()->getEntityPersister($parentClassName);
                $subTableStmts[$parentTableName] = $this->_conn->prepare($parentPersister->_getInsertSQL());
            }
        }

        // Execute all inserts. For each entity:
        // 1) Insert on root table
        // 2) Insert on sub tables
        foreach ($this->_queuedInserts as $entity) {
            $insertData = $this->_prepareInsertData($entity);

            // Execute insert on root table
            $paramIndex = 1;
            foreach ($insertData[$rootTableName] as $columnName => $value) {
                $rootTableStmt->bindValue($paramIndex++, $value, $this->_columnTypes[$columnName]);
            }
            $rootTableStmt->execute();

            if ($isPostInsertId) {
                $id = $idGen->generate($this->_em, $entity);
                $postInsertIds[$id] = $entity;
            } else {
                $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
            }

            // Execute inserts on subtables.
            // The order doesn't matter because all child tables link to the root table via FK.
            foreach ($subTableStmts as $tableName => $stmt) {
                $data = isset($insertData[$tableName]) ? $insertData[$tableName] : array();
                $paramIndex = 1;
                foreach ((array) $id as $idVal) {
                    $stmt->bindValue($paramIndex++, $idVal);
                }
                foreach ($data as $columnName => $value) {
                    $stmt->bindValue($paramIndex++, $value, $this->_columnTypes[$columnName]);
                }
                $stmt->execute();
            }
        }

        $rootTableStmt->closeCursor();
        foreach ($subTableStmts as $stmt) {
            $stmt->closeCursor();
        }

        if ($this->_class->isVersioned) {
            $this->assignDefaultVersionValue($entity, $id);
        }

        $this->_queuedInserts = array();

        return $postInsertIds;
    }

    /**
     * {@inheritdoc}
     */
    public function update($entity)
    {
        $updateData = $this->_prepareUpdateData($entity);

        if ($isVersioned = $this->_class->isVersioned) {
            $versionedClass = $this->_getVersionedClassMetadata();
            $versionedTable = $versionedClass->table['name'];
        }

        if ($updateData) {
            foreach ($updateData as $tableName => $data) {
                $this->_updateTable($entity, $this->_quotedTableMap[$tableName], $data, $isVersioned && $versionedTable == $tableName);
            }
            // Make sure the table with the version column is updated even if no columns on that
            // table were affected.
            if ($isVersioned && ! isset($updateData[$versionedTable])) {
                $this->_updateTable($entity, $versionedClass->getQuotedTableName($this->_platform), array(), true);

                $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
                $this->assignDefaultVersionValue($entity, $id);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function delete($entity)
    {
        $identifier = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
        $this->deleteJoinTableRecords($identifier);

        $id = array_combine($this->_class->getIdentifierColumnNames(), $identifier);

        // If the database platform supports FKs, just
        // delete the row from the root table. Cascades do the rest.
        if ($this->_platform->supportsForeignKeyConstraints()) {
            $this->_conn->delete($this->_em->getClassMetadata($this->_class->rootEntityName)
                    ->getQuotedTableName($this->_platform), $id);
        } else {
            // Delete from all tables individually, starting from this class' table up to the root table.
            $this->_conn->delete($this->_class->getQuotedTableName($this->_platform), $id);
            foreach ($this->_class->parentClasses as $parentClass) {
                $this->_conn->delete($this->_em->getClassMetadata($parentClass)->getQuotedTableName($this->_platform), $id);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null)
    {
        $idColumns = $this->_class->getIdentifierColumnNames();
        $baseTableAlias = $this->_getSQLTableAlias($this->_class->name);

        // Create the column list fragment only once
        if ($this->_selectColumnListSql === null) {

            $this->_rsm = new ResultSetMapping();
            $this->_rsm->addEntityResult($this->_class->name, 'r');

            // Add regular columns
            $columnList = '';
            foreach ($this->_class->fieldMappings as $fieldName => $mapping) {
                if ($columnList != '') $columnList .= ', ';
                $columnList .= $this->_getSelectColumnSQL($fieldName,
                        isset($mapping['inherited']) ?
                        $this->_em->getClassMetadata($mapping['inherited']) :
                        $this->_class);
            }

            // Add foreign key columns
            foreach ($this->_class->associationMappings as $assoc2) {
                if ($assoc2['isOwningSide'] && $assoc2['type'] & ClassMetadata::TO_ONE) {
                    $tableAlias = isset($assoc2['inherited']) ?
                            $this->_getSQLTableAlias($assoc2['inherited'])
                            : $baseTableAlias;
                    foreach ($assoc2['targetToSourceKeyColumns'] as $srcColumn) {
                        if ($columnList != '') $columnList .= ', ';
                        $columnList .= $this->getSelectJoinColumnSQL($tableAlias, $srcColumn,
                            isset($assoc2['inherited']) ? $assoc2['inherited'] : $this->_class->name
                        );
                    }
                }
            }

            // Add discriminator column (DO NOT ALIAS, see AbstractEntityInheritancePersister#_processSQLResult).
            $discrColumn = $this->_class->discriminatorColumn['name'];
            if ($this->_class->rootEntityName == $this->_class->name) {
                $columnList .= ", $baseTableAlias.$discrColumn";
            } else {
                $columnList .= ', ' . $this->_getSQLTableAlias($this->_class->rootEntityName)
                        . ".$discrColumn";
            }

            $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn);
            $this->_rsm->setDiscriminatorColumn('r', $resultColumnName);
            $this->_rsm->addMetaResult('r', $resultColumnName, $discrColumn);
        }

        // INNER JOIN parent tables
        $joinSql = '';
        foreach ($this->_class->parentClasses as $parentClassName) {
            $parentClass = $this->_em->getClassMetadata($parentClassName);
            $tableAlias = $this->_getSQLTableAlias($parentClassName);
            $joinSql .= ' INNER JOIN ' . $parentClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON ';
            $first = true;
            foreach ($idColumns as $idColumn) {
                if ($first) $first = false; else $joinSql .= ' AND ';
                $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
            }
        }

        // OUTER JOIN sub tables
        foreach ($this->_class->subClasses as $subClassName) {
            $subClass = $this->_em->getClassMetadata($subClassName);
            $tableAlias = $this->_getSQLTableAlias($subClassName);

            if ($this->_selectColumnListSql === null) {
                // Add subclass columns
                foreach ($subClass->fieldMappings as $fieldName => $mapping) {
                    if (isset($mapping['inherited'])) {
                        continue;
                    }
                    $columnList .= ', ' . $this->_getSelectColumnSQL($fieldName, $subClass);
                }

                // Add join columns (foreign keys)
                foreach ($subClass->associationMappings as $assoc2) {
                    if ($assoc2['isOwningSide'] && $assoc2['type'] & ClassMetadata::TO_ONE
                            && ! isset($assoc2['inherited'])) {
                        foreach ($assoc2['targetToSourceKeyColumns'] as $srcColumn) {
                            if ($columnList != '') $columnList .= ', ';
                            $columnList .= $this->getSelectJoinColumnSQL($tableAlias, $srcColumn,
                                isset($assoc2['inherited']) ? $assoc2['inherited'] : $subClass->name
                            );
                        }
                    }
                }
            }

            // Add LEFT JOIN
            $joinSql .= ' LEFT JOIN ' . $subClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON ';
            $first = true;
            foreach ($idColumns as $idColumn) {
                if ($first) $first = false; else $joinSql .= ' AND ';
                $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
            }
        }

        $joinSql .= $assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY ?
                $this->_getSelectManyToManyJoinSQL($assoc) : '';

        $conditionSql = $this->_getSelectConditionSQL($criteria, $assoc);

        $orderBy = ($assoc !== null && isset($assoc['orderBy'])) ? $assoc['orderBy'] : $orderBy;
        $orderBySql = $orderBy ? $this->_getOrderBySQL($orderBy, $baseTableAlias) : '';

        if ($this->_selectColumnListSql === null) {
            $this->_selectColumnListSql = $columnList;
        }

        $lockSql = '';
        if ($lockMode == LockMode::PESSIMISTIC_READ) {
            $lockSql = ' ' . $this->_platform->getReadLockSql();
        } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) {
            $lockSql = ' ' . $this->_platform->getWriteLockSql();
        }

        return $this->_platform->modifyLimitQuery('SELECT ' . $this->_selectColumnListSql
                . ' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $baseTableAlias
                . $joinSql
                . ($conditionSql != '' ? ' WHERE ' . $conditionSql : '') . $orderBySql, $limit, $offset)
                . $lockSql;
    }

    /**
     * Get the FROM and optionally JOIN conditions to lock the entity managed by this persister.
     *
     * @return string
     */
    public function getLockTablesSql()
    {
        $idColumns = $this->_class->getIdentifierColumnNames();
        $baseTableAlias = $this->_getSQLTableAlias($this->_class->name);

        // INNER JOIN parent tables
        $joinSql = '';
        foreach ($this->_class->parentClasses as $parentClassName) {
            $parentClass = $this->_em->getClassMetadata($parentClassName);
            $tableAlias = $this->_getSQLTableAlias($parentClassName);
            $joinSql .= ' INNER JOIN ' . $parentClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON ';
            $first = true;
            foreach ($idColumns as $idColumn) {
                if ($first) $first = false; else $joinSql .= ' AND ';
                $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
            }
        }

        return 'FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $baseTableAlias . $joinSql;
    }

    /* Ensure this method is never called. This persister overrides _getSelectEntitiesSQL directly. */
    protected function _getSelectColumnListSQL()
    {
        throw new \BadMethodCallException("Illegal invocation of ".__METHOD__.".");
    }

    /** {@inheritdoc} */
    protected function _getInsertColumnList()
    {
        // Identifier columns must always come first in the column list of subclasses.
        $columns = $this->_class->parentClasses ? $this->_class->getIdentifierColumnNames() : array();

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

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

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

        return $columns;
    }

    /**
     * {@inheritdoc}
     */
    protected function assignDefaultVersionValue($entity, $id)
    {
        $value = $this->fetchVersionValue($this->_getVersionedClassMetadata(), $id);
        $this->_class->setFieldValue($entity, $this->_class->versionField, $value);
    }
}


text:
<?php
/*
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.doctrine-project.org>.
 */

namespace Doctrine\ORM\Persisters;

use Doctrine\ORM\EntityManager,
    Doctrine\ORM\PersistentCollection;

/**
 * Base class for all collection persisters.
 *
 * @since 2.0
 * @author Roman Borschel <roman@code-factory.org>
 */
abstract class AbstractCollectionPersister
{
    /**
     * @var EntityManager
     */
    protected $_em;

    /**
     * @var Doctrine\DBAL\Connection
     */
    protected $_conn;

    /**
     * @var Doctrine\ORM\UnitOfWork
     */
    protected $_uow;

    /**
     * Initializes a new instance of a class derived from AbstractCollectionPersister.
     *
     * @param Doctrine\ORM\EntityManager $em
     */
    public function __construct(EntityManager $em)
    {
        $this->_em = $em;
        $this->_uow = $em->getUnitOfWork();
        $this->_conn = $em->getConnection();
    }

    /**
     * Deletes the persistent state represented by the given collection.
     *
     * @param PersistentCollection $coll
     */
    public function delete(PersistentCollection $coll)
    {
        $mapping = $coll->getMapping();
        if ( ! $mapping['isOwningSide']) {
            return; // ignore inverse side
        }
        $sql = $this->_getDeleteSQL($coll);
        $this->_conn->executeUpdate($sql, $this->_getDeleteSQLParameters($coll));
    }

    /**
     * Gets the SQL statement for deleting the given collection.
     *
     * @param PersistentCollection $coll
     */
    abstract protected function _getDeleteSQL(PersistentCollection $coll);

    /**
     * Gets the SQL parameters for the corresponding SQL statement to delete
     * the given collection.
     *
     * @param PersistentCollection $coll
     */
    abstract protected function _getDeleteSQLParameters(PersistentCollection $coll);

    /**
     * Updates the given collection, synchronizing it's state with the database
     * by inserting, updating and deleting individual elements.
     *
     * @param PersistentCollection $coll
     */
    public function update(PersistentCollection $coll)
    {
        $mapping = $coll->getMapping();
        if ( ! $mapping['isOwningSide']) {
            return; // ignore inverse side
        }
        $this->deleteRows($coll);
        //$this->updateRows($coll);
        $this->insertRows($coll);
    }

    public function deleteRows(PersistentCollection $coll)
    {
        $deleteDiff = $coll->getDeleteDiff();
        $sql = $this->_getDeleteRowSQL($coll);
        foreach ($deleteDiff as $element) {
            $this->_conn->executeUpdate($sql, $this->_getDeleteRowSQLParameters($coll, $element));
        }
    }

    //public function updateRows(PersistentCollection $coll)
    //{}

    public function insertRows(PersistentCollection $coll)
    {
        $insertDiff = $coll->getInsertDiff();
        $sql = $this->_getInsertRowSQL($coll);
        foreach ($insertDiff as $element) {
            $this->_conn->executeUpdate($sql, $this->_getInsertRowSQLParameters($coll, $element));
        }
    }

    public function count(PersistentCollection $coll)
    {
        throw new \BadMethodCallException("Counting the size of this persistent collection is not supported by this CollectionPersister.");
    }

    public function slice(PersistentCollection $coll, $offset, $length = null)
    {
        throw new \BadMethodCallException("Slicing elements is not supported by this CollectionPersister.");
    }

    public function contains(PersistentCollection $coll, $element)
    {
        throw new \BadMethodCallException("Checking for existance of an element is not supported by this CollectionPersister.");
    }

    public function containsKey(PersistentCollection $coll, $key)
    {
        throw new \BadMethodCallException("Checking for existance of a key is not supported by this CollectionPersister.");
    }

    public function get(PersistentCollection $coll, $index)
    {
        throw new \BadMethodCallException("Selecting a collection by index is not supported by this CollectionPersister.");
    }

    /**
     * Gets the SQL statement used for deleting a row from the collection.
     *
     * @param PersistentCollection $coll
     */
    abstract protected function _getDeleteRowSQL(PersistentCollection $coll);

    /**
     * Gets the SQL parameters for the corresponding SQL statement to delete the given
     * element from the given collection.
     *
     * @param PersistentCollection $coll
     * @param mixed $element
     */
    abstract protected function _getDeleteRowSQLParameters(PersistentCollection $coll, $element);

    /**
     * Gets the SQL statement used for updating a row in the collection.
     *
     * @param PersistentCollection $coll
     */
    abstract protected function _getUpdateRowSQL(PersistentCollection $coll);

    /**
     * Gets the SQL statement used for inserting a row in the collection.
     *
     * @param PersistentCollection $coll
     */
    abstract protected function _getInsertRowSQL(PersistentCollection $coll);

    /**
     * Gets the SQL parameters for the corresponding SQL statement to insert the given
     * element of the given collection into the database.
     *
     * @param PersistentCollection $coll
     * @param mixed $element
     */
    abstract protected function _getInsertRowSQLParameters(PersistentCollection $coll, $element);
}