/** * @dataProvider lockDataProvider */ public function testLock($isVersioned, $expectsException) { $object = new VersionedEntity(); $em = $this->getMockBuilder('Doctrine\\ORM\\EntityManager')->disableOriginalConstructor()->setMethods(array('lock'))->getMock(); $modelManager = $this->getMockBuilder('Sonata\\DoctrineORMAdminBundle\\Model\\ModelManager')->disableOriginalConstructor()->setMethods(array('getMetadata', 'getEntityManager'))->getMock(); $modelManager->expects($this->any())->method('getEntityManager')->will($this->returnValue($em)); $metadata = $this->getMetadata(get_class($object), $isVersioned); $modelManager->expects($this->any())->method('getMetadata')->will($this->returnValue($metadata)); if ($expectsException) { $em->expects($this->once())->method('lock')->will($this->throwException(OptimisticLockException::lockFailed($object))); $this->setExpectedException('Sonata\\AdminBundle\\Exception\\LockException'); } $modelManager->lock($object, 123); }
/** * 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); } }
/** * {@inheritdoc} */ public function walkSelectStatement(AST\SelectStatement $AST) { $sql = $this->walkSelectClause($AST->selectClause); $sql .= $this->walkFromClause($AST->fromClause); $sql .= $this->walkWhereClause($AST->whereClause); $sql .= $AST->groupByClause ? $this->walkGroupByClause($AST->groupByClause) : ''; $sql .= $AST->havingClause ? $this->walkHavingClause($AST->havingClause) : ''; if (($orderByClause = $AST->orderByClause) !== null) { $sql .= $AST->orderByClause ? $this->walkOrderByClause($AST->orderByClause) : ''; } else { if (($orderBySql = $this->_generateOrderedCollectionOrderByItems()) !== '') { $sql .= ' ORDER BY ' . $orderBySql; } } $sql = $this->platform->modifyLimitQuery($sql, $this->query->getMaxResults(), $this->query->getFirstResult()); if (($lockMode = $this->query->getHint(Query::HINT_LOCK_MODE)) !== false) { switch ($lockMode) { case LockMode::PESSIMISTIC_READ: $sql .= ' ' . $this->platform->getReadLockSQL(); break; case LockMode::PESSIMISTIC_WRITE: $sql .= ' ' . $this->platform->getWriteLockSQL(); break; case LockMode::OPTIMISTIC: foreach ($this->selectedClasses as $selectedClass) { if (!$selectedClass['class']->isVersioned) { throw \Doctrine\ORM\OptimisticLockException::lockFailed($selectedClass['class']->name); } } break; case LockMode::NONE: break; default: throw \Doctrine\ORM\Query\QueryException::invalidLockMode(); } } return $sql; }
/** * 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. * * @return void * * @throws \Doctrine\ORM\ORMException * @throws \Doctrine\ORM\OptimisticLockException */ protected final function updateTable($entity, $quotedTableName, array $updateData, $versioned = false) { $set = array(); $types = array(); $params = array(); foreach ($updateData as $columnName => $value) { $placeholder = '?'; $column = $columnName; switch (true) { case isset($this->class->fieldNames[$columnName]): $fieldName = $this->class->fieldNames[$columnName]; $column = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform); if (isset($this->class->fieldMappings[$fieldName]['requireSQLConversion'])) { $type = Type::getType($this->columnTypes[$columnName]); $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform); } break; case isset($this->quotedColumns[$columnName]): $column = $this->quotedColumns[$columnName]; break; } $params[] = $value; $set[] = $column . ' = ' . $placeholder; $types[] = $this->columnTypes[$columnName]; } $where = array(); $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity); foreach ($this->class->identifier as $idField) { if ( ! isset($this->class->associationMappings[$idField])) { $params[] = $identifier[$idField]; $types[] = $this->class->fieldMappings[$idField]['type']; $where[] = $this->quoteStrategy->getColumnName($idField, $this->class, $this->platform); continue; } $params[] = $identifier[$idField]; $where[] = $this->class->associationMappings[$idField]['joinColumns'][0]['name']; $targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]['targetEntity']); switch (true) { case (isset($targetMapping->fieldMappings[$targetMapping->identifier[0]])): $types[] = $targetMapping->fieldMappings[$targetMapping->identifier[0]]['type']; break; case (isset($targetMapping->associationMappings[$targetMapping->identifier[0]])): $types[] = $targetMapping->associationMappings[$targetMapping->identifier[0]]['type']; break; default: throw ORMException::unrecognizedField($targetMapping->identifier[0]); } } if ($versioned) { $versionField = $this->class->versionField; $versionFieldType = $this->class->fieldMappings[$versionField]['type']; $versionColumn = $this->quoteStrategy->getColumnName($versionField, $this->class, $this->platform); $where[] = $versionColumn; $types[] = $this->class->fieldMappings[$versionField]['type']; $params[] = $this->class->reflFields[$versionField]->getValue($entity); switch ($versionFieldType) { case Type::INTEGER: $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1'; break; case Type::DATETIME: $set[] = $versionColumn . ' = CURRENT_TIMESTAMP'; break; } } $sql = 'UPDATE ' . $quotedTableName . ' SET ' . implode(', ', $set) . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?'; $result = $this->conn->executeUpdate($sql, $params, $types); if ($versioned && ! $result) { throw OptimisticLockException::lockFailed($entity); } }
/** * {@inheritdoc} */ public function walkSelectStatement(AST\SelectStatement $AST) { $limit = $this->query->getMaxResults(); $offset = $this->query->getFirstResult(); $lockMode = $this->query->getHint(Query::HINT_LOCK_MODE); $sql = $this->walkSelectClause($AST->selectClause) . $this->walkFromClause($AST->fromClause) . $this->walkWhereClause($AST->whereClause); if ($AST->groupByClause) { $sql .= $this->walkGroupByClause($AST->groupByClause); } if ($AST->havingClause) { $sql .= $this->walkHavingClause($AST->havingClause); } if ($AST->orderByClause) { $sql .= $this->walkOrderByClause($AST->orderByClause); } if (!$AST->orderByClause && ($orderBySql = $this->_generateOrderedCollectionOrderByItems())) { $sql .= ' ORDER BY ' . $orderBySql; } if ($limit !== null || $offset !== null) { $sql = $this->platform->modifyLimitQuery($sql, $limit, $offset); } if ($lockMode === null || $lockMode === false || $lockMode === LockMode::NONE) { return $sql; } if ($lockMode === LockMode::PESSIMISTIC_READ) { return $sql . ' ' . $this->platform->getReadLockSQL(); } if ($lockMode === LockMode::PESSIMISTIC_WRITE) { return $sql . ' ' . $this->platform->getWriteLockSQL(); } if ($lockMode !== LockMode::OPTIMISTIC) { throw QueryException::invalidLockMode(); } foreach ($this->selectedClasses as $selectedClass) { if (!$selectedClass['class']->isVersioned) { throw OptimisticLockException::lockFailed($selectedClass['class']->name); } } return $sql; }
/** * Walks down a SelectStatement AST node, thereby generating the appropriate SQL. * * @return string The SQL. */ public function walkSelectStatement(AST\SelectStatement $AST) { $sql = $this->walkSelectClause($AST->selectClause); $sql .= $this->walkFromClause($AST->fromClause); if (($whereClause = $AST->whereClause) !== null) { $sql .= $this->walkWhereClause($whereClause); } else { if (($discSql = $this->_generateDiscriminatorColumnConditionSQL($this->_rootAliases)) !== '') { $sql .= ' WHERE ' . $discSql; } } $sql .= $AST->groupByClause ? $this->walkGroupByClause($AST->groupByClause) : ''; $sql .= $AST->havingClause ? $this->walkHavingClause($AST->havingClause) : ''; if (($orderByClause = $AST->orderByClause) !== null) { $sql .= $AST->orderByClause ? $this->walkOrderByClause($AST->orderByClause) : ''; } else { if (($orderBySql = $this->_generateOrderedCollectionOrderByItems()) !== '') { $sql .= ' ORDER BY ' . $orderBySql; } } $sql = $this->_platform->modifyLimitQuery($sql, $this->_query->getMaxResults(), $this->_query->getFirstResult()); if (($lockMode = $this->_query->getHint(Query::HINT_LOCK_MODE)) !== false) { if ($lockMode == LockMode::PESSIMISTIC_READ) { $sql .= " " . $this->_platform->getReadLockSQL(); } else { if ($lockMode == LockMode::PESSIMISTIC_WRITE) { $sql .= " " . $this->_platform->getWriteLockSQL(); } else { if ($lockMode == LockMode::OPTIMISTIC) { foreach ($this->_selectedClasses as $class) { if (!$class->isVersioned) { throw \Doctrine\ORM\OptimisticLockException::lockFailed(); } } } } } } return $sql; }
/** * Perform UPDATE statement for an entity. This function has support for * optimistic locking if the entities ClassMetadata has versioning enabled. * * @param object $entity The entity object being updated * @param string $tableName The name of the table being updated * @param array $data The array of data to set * @param array $where The condition used to update * @return void */ protected function _doUpdate($entity, $tableName, $data, $where) { // Note: $tableName and column names in $data are already quoted for SQL. $set = array(); foreach ($data as $columnName => $value) { $set[] = $columnName . ' = ?'; } if ($isVersioned = $this->_class->isVersioned) { $versionField = $this->_class->versionField; $versionFieldType = $this->_class->getTypeOfField($versionField); $where[$versionField] = Type::getType($versionFieldType)->convertToDatabaseValue($this->_class->reflFields[$versionField]->getValue($entity), $this->_platform); $versionFieldColumnName = $this->_class->getQuotedColumnName($versionField, $this->_platform); if ($versionFieldType == 'integer') { $set[] = $versionFieldColumnName . ' = ' . $versionFieldColumnName . ' + 1'; } else { if ($versionFieldType == 'datetime') { $set[] = $versionFieldColumnName . ' = CURRENT_TIMESTAMP'; } } } $params = array_merge(array_values($data), array_values($where)); $sql = 'UPDATE ' . $tableName . ' SET ' . implode(', ', $set) . ' WHERE ' . implode(' = ? AND ', array_keys($where)) . ' = ?'; $result = $this->_conn->executeUpdate($sql, $params); if ($isVersioned && !$result) { throw \Doctrine\ORM\OptimisticLockException::lockFailed(); } }
/** * Executes a merge operation on an entity. * * @param object $entity * @param array $visited * @return object The managed copy of the entity. * @throws OptimisticLockException If the entity uses optimistic locking through a version * attribute and the version check against the managed copy fails. * @throws InvalidArgumentException If the entity instance is NEW. */ private function _doMerge($entity, array &$visited, $prevManagedCopy = null, $assoc = null) { $class = $this->_em->getClassMetadata(get_class($entity)); $id = $class->getIdentifierValues($entity); if (!$id) { throw new \InvalidArgumentException('New entity detected during merge.' . ' Persist the new entity before merging.'); } // MANAGED entities are ignored by the merge operation if ($this->getEntityState($entity, self::STATE_DETACHED) == self::STATE_MANAGED) { $managedCopy = $entity; } else { // Try to look the entity up in the identity map. $managedCopy = $this->tryGetById($id, $class->rootEntityName); if ($managedCopy) { // We have the entity in-memory already, just make sure its not removed. if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) { throw new \InvalidArgumentException('Removed entity detected during merge.' . ' Can not merge with a removed entity.'); } } else { // We need to fetch the managed copy in order to merge. $managedCopy = $this->_em->find($class->name, $id); } if ($managedCopy === null) { throw new \InvalidArgumentException('New entity detected during merge.' . ' Persist the new entity before merging.'); } if ($class->isVersioned) { $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy); $entityVersion = $class->reflFields[$class->versionField]->getValue($entity); // Throw exception if versions dont match. if ($managedCopyVersion != $entityVersion) { throw OptimisticLockException::lockFailed(); } } // Merge state of $entity into existing (managed) entity foreach ($class->reflFields as $name => $prop) { if (!isset($class->associationMappings[$name])) { $prop->setValue($managedCopy, $prop->getValue($entity)); } else { $assoc2 = $class->associationMappings[$name]; if ($assoc2->isOneToOne()) { if (!$assoc2->isCascadeMerge) { $other = $class->reflFields[$name]->getValue($entity); //TODO: Just $prop->getValue($entity)? if ($other !== null) { $targetClass = $this->_em->getClassMetadata($assoc2->targetEntityName); $id = $targetClass->getIdentifierValues($other); $proxy = $this->_em->getProxyFactory()->getProxy($assoc2->targetEntityName, $id); $prop->setValue($managedCopy, $proxy); $this->registerManaged($proxy, $id, array()); } } } else { $coll = new PersistentCollection($this->_em, $this->_em->getClassMetadata($assoc2->targetEntityName), new ArrayCollection()); $coll->setOwner($managedCopy, $assoc2); $coll->setInitialized($assoc2->isCascadeMerge); $prop->setValue($managedCopy, $coll); } } if ($class->isChangeTrackingNotify()) { //TODO: put changed fields in changeset...? } } if ($class->isChangeTrackingDeferredExplicit()) { //TODO: Mark $managedCopy for dirty check...? ($this->_scheduledForDirtyCheck) } } if ($prevManagedCopy !== null) { $assocField = $assoc->sourceFieldName; $prevClass = $this->_em->getClassMetadata(get_class($prevManagedCopy)); if ($assoc->isOneToOne()) { $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy); //TODO: What about back-reference if bidirectional? } else { $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->unwrap()->add($managedCopy); if ($assoc->isOneToMany()) { $class->reflFields[$assoc->mappedBy]->setValue($managedCopy, $prevManagedCopy); } } } $this->_cascadeMerge($entity, $managedCopy, $visited); return $managedCopy; }