diff --git a/lib/Doctrine/DBAL/Connection.php b/lib/Doctrine/DBAL/Connection.php index e3b01d438..2e6630e47 100644 --- a/lib/Doctrine/DBAL/Connection.php +++ b/lib/Doctrine/DBAL/Connection.php @@ -746,8 +746,9 @@ class Connection if ($this->_transactionNestingLevel == 1) { $this->_transactionNestingLevel = 0; $this->_conn->rollback(); + } else { + --$this->_transactionNestingLevel; } - --$this->_transactionNestingLevel; return true; } diff --git a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php index bf0faebb9..15600cefc 100644 --- a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php @@ -850,7 +850,7 @@ abstract class AbstractPlatform $default = empty($field['notnull']) ? ' DEFAULT NULL' : ''; if (isset($field['default'])) { - $default = ' DEFAULT ' . $this->quote($field['default'], $field['type']); + $default = ' DEFAULT ' . $this->quoteIdentifier($field['default'], $field['type']); } return $default; } diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 94037a3f0..554e17d7e 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -189,7 +189,7 @@ class EntityManager if ($this->_flushMode == self::FLUSHMODE_AUTO || $this->_flushMode == self::FLUSHMODE_COMMIT) { $this->flush(); } - $this->_conn->commitTransaction(); + $this->_conn->commit(); } /** diff --git a/lib/Doctrine/ORM/Events.php b/lib/Doctrine/ORM/Events.php index 2668671fc..0f2a732e4 100644 --- a/lib/Doctrine/ORM/Events.php +++ b/lib/Doctrine/ORM/Events.php @@ -35,8 +35,8 @@ final class Events const preDelete = 'preDelete'; const postDelete = 'postDelete'; - const preInsert = 'preSave'; - const postInsert = 'postSave'; + const preInsert = 'preInsert'; + const postInsert = 'postInsert'; const preUpdate = 'preUpdate'; const postUpdate = 'postUpdate'; const load = 'load'; diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadata.php b/lib/Doctrine/ORM/Mapping/ClassMetadata.php index 826feb336..c95045e05 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadata.php @@ -397,6 +397,20 @@ final class ClassMetadata */ public $inheritedAssociationFields = array(); + /** + * A flag for whether or not the model is to be versioned with optimistic locking + * + * @var boolean $isVersioned + */ + public $isVersioned; + + /** + * The name of the field which stores the version information + * + * @var mixed $versionField + */ + public $versionField; + /** * Initializes a new ClassMetadata instance that will hold the object-relational mapping * metadata of the class with the given name. @@ -1758,6 +1772,24 @@ final class ClassMetadata $this->sequenceGeneratorDefinition = $definition; } + public function isVersioned($bool = null) + { + if ( ! is_null($bool)) { + $this->isVersioned = $bool; + } + return $this->isVersioned; + } + + public function getVersionField() + { + return $this->versionField; + } + + public function setVersionField($versionField) + { + $this->versionField = $versionField; + } + /** * Creates a string representation of this instance. * diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index 4290fdddc..ac3ea681c 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -162,6 +162,8 @@ class ClassMetadataFactory $this->_addInheritedFields($class, $parent); $this->_addInheritedRelations($class, $parent); $class->setIdentifier($parent->identifier); + $class->isVersioned($parent->isVersioned); + $class->setVersionField($parent->versionField); } // Invoke driver @@ -259,13 +261,18 @@ class ClassMetadataFactory */ private function _generateStaticSql($class) { + if ($versioned = $class->isVersioned()) { + $versionField = $class->getVersionField(); + } + // Generate INSERT SQL $columns = $values = array(); if ($class->inheritanceType == ClassMetadata::INHERITANCE_TYPE_JOINED) { // Generate INSERT SQL for inheritance type JOINED foreach ($class->reflFields as $name => $field) { if (isset($class->fieldMappings[$name]['inherited']) && ! isset($class->fieldMappings[$name]['id']) - || isset($class->inheritedAssociationFields[$name])) { + || isset($class->inheritedAssociationFields[$name]) + || ($versioned && $versionField == $name)) { continue; } @@ -285,6 +292,9 @@ class ClassMetadataFactory } else { // Generate INSERT SQL for inheritance types NONE, SINGLE_TABLE, TABLE_PER_CLASS foreach ($class->reflFields as $name => $field) { + if ($versioned && $versionField == $name) { + continue; + } if (isset($class->associationMappings[$name])) { $assoc = $class->associationMappings[$name]; if ($assoc->isOwningSide && $assoc->isOneToOne()) { diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 041c83c21..bed4aff56 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -144,6 +144,9 @@ class AnnotationDriver implements Driver $mapping['type'] = $columnAnnot->type; $mapping['length'] = $columnAnnot->length; $mapping['nullable'] = $columnAnnot->nullable; + if (isset($columnAnnot->default)) { + $mapping['default'] = $columnAnnot->default; + } if (isset($columnAnnot->name)) { $mapping['columnName'] = $columnAnnot->name; } @@ -153,6 +156,17 @@ class AnnotationDriver implements Driver if ($generatedValueAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\GeneratedValue')) { $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $generatedValueAnnot->strategy)); } + if ($versionAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Version')) { + $metadata->isVersioned(true); + $metadata->setVersionField($mapping['fieldName']); + + if ( ! isset($mapping['default'])) { + // TODO: When we have timestamp optimistic locking + // we'll have to figure out a better way to do this? + // Can we set the default value to be NOW() ? + $mapping['default'] = 1; + } + } $metadata->mapField($mapping); // Check for SequenceGenerator/TableGenerator definition @@ -165,7 +179,6 @@ class AnnotationDriver implements Driver } else if ($tblGeneratorAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\TableGenerator')) { throw new DoctrineException("DoctrineTableGenerator not yet implemented."); } - } else if ($oneToOneAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OneToOne')) { $mapping['targetEntity'] = $oneToOneAnnot->targetEntity; $mapping['joinColumns'] = $joinColumns; diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index bdc49c5a5..288a06ff4 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -54,6 +54,7 @@ final class Column extends \Doctrine\Common\Annotations\Annotation { public $length; public $unique = false; public $nullable = false; + public $default; public $name; } final class OneToOne extends \Doctrine\Common\Annotations\Annotation { @@ -95,10 +96,9 @@ final class JoinTable extends \Doctrine\Common\Annotations\Annotation { public $inverseJoinColumns; } final class SequenceGenerator extends \Doctrine\Common\Annotations\Annotation { - //public $name; public $sequenceName; public $allocationSize = 10; public $initialValue = 1; } final class ChangeTrackingPolicy extends \Doctrine\Common\Annotations\Annotation {} -final class DoctrineX extends \Doctrine\Common\Annotations\Annotation {} +final class DoctrineX extends \Doctrine\Common\Annotations\Annotation {} \ No newline at end of file diff --git a/lib/Doctrine/ORM/OptimisticLockException.php b/lib/Doctrine/ORM/OptimisticLockException.php new file mode 100644 index 000000000..486196f21 --- /dev/null +++ b/lib/Doctrine/ORM/OptimisticLockException.php @@ -0,0 +1,35 @@ +. + */ + +namespace Doctrine\ORM; + +/** + * EntityManagerException + * + * @author Konsta Vesterinen + * @author Roman Borschel + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + */ +class OptimisticLockException extends \Doctrine\Common\DoctrineException +{} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index d108c1a24..c98f1bb77 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -94,6 +94,15 @@ class JoinedSubclassPersister extends StandardEntityPersister return; } + if ($isVersioned = $this->_class->isVersioned) { + if (isset($this->_class->fieldMappings[$this->_class->versionField]['inherited'])) { + $definingClassName = $this->_class->fieldMappings[$this->_class->versionField]['inherited']; + $versionedClass = $this->_em->getClassMetadata($definingClassName); + } else { + $versionedClass = $this->_class; + } + } + $postInsertIds = array(); $idGen = $this->_class->idGenerator; $isPostInsertId = $idGen->isPostInsertGenerator(); @@ -170,6 +179,10 @@ class JoinedSubclassPersister extends StandardEntityPersister foreach ($stmts as $stmt) $stmt->closeCursor(); + if ($isVersioned) { + $this->_assignDefaultVersionValue($versionedClass, $entity, $id); + } + $this->_queuedInserts = array(); return $postInsertIds; @@ -192,7 +205,7 @@ class JoinedSubclassPersister extends StandardEntityPersister ); foreach ($updateData as $tableName => $data) { - $this->_conn->update($tableName, $updateData[$tableName], $id); + $this->_doUpdate($entity, $tableName, $updateData[$tableName], $id); } } diff --git a/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php b/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php index 618937edf..eb0469363 100644 --- a/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php @@ -122,6 +122,8 @@ class StandardEntityPersister return; } + $isVersioned = $this->_class->isVersioned; + $postInsertIds = array(); $idGen = $this->_class->idGenerator; $isPostInsertId = $idGen->isPostInsertGenerator(); @@ -159,7 +161,14 @@ class StandardEntityPersister $stmt->execute(); if ($isPostInsertId) { - $postInsertIds[$idGen->generate($this->_em, $entity)] = $entity; + $id = $idGen->generate($this->_em, $entity); + $postInsertIds[$id] = $entity; + } else { + $id = $this->_class->getIdentifierValues($entity); + } + + if ($isVersioned) { + $this->_assignDefaultVersionValue($this->_class, $entity, $id); } if ($hasPostInsertListeners) { @@ -173,6 +182,18 @@ class StandardEntityPersister return $postInsertIds; } + protected function _assignDefaultVersionValue($class, $entity, $id) + { + $versionField = $this->_class->getVersionField(); + $identifier = $this->_class->getIdentifierColumnNames(); + $versionFieldColumnName = $this->_class->getColumnName($versionField); + + $sql = "SELECT " . $versionFieldColumnName . " FROM " . $class->primaryTable['name'] . + " WHERE " . implode(' = ? AND ', $identifier) . " = ?"; + $value = $this->_conn->fetchColumn($sql, (array) $id); + $this->_class->setFieldValue($entity, $versionField, $value[0]); + } + /** * Updates an entity. * @@ -192,13 +213,44 @@ class StandardEntityPersister $this->_preUpdate($entity); } - $this->_conn->update($tableName, $updateData[$tableName], $id); + if (isset($updateData[$tableName]) && $updateData[$tableName]) { + $this->_doUpdate($entity, $tableName, $updateData[$tableName], $id); + } if ($this->_evm->hasListeners(Events::postUpdate)) { $this->_postUpdate($entity); } } + protected function _doUpdate($entity, $tableName, $data, $where) + { + $set = array(); + foreach ($data as $columnName => $value) { + $set[] = $this->_conn->quoteIdentifier($columnName) . ' = ?'; + } + + if ($versioned = $this->_class->isVersioned()) { + $versionField = $this->_class->getVersionField(); + $identifier = $this->_class->getIdentifier(); + $versionFieldColumnName = $this->_class->getColumnName($versionField); + $where[$versionFieldColumnName] = $entity->version; + $set[] = $this->_conn->quoteIdentifier($versionFieldColumnName) . ' = ' . + $this->_conn->quoteIdentifier($versionFieldColumnName) . ' + 1'; + } + $params = array_merge(array_values($data), array_values($where)); + + $sql = 'UPDATE ' . $this->_conn->quoteIdentifier($tableName) + . ' SET ' . implode(', ', $set) + . ' WHERE ' . implode(' = ? AND ', array_keys($where)) + . ' = ?'; + + $result = $this->_conn->exec($sql, $params); + + if ($versioned && ! $result) { + throw \Doctrine\ORM\OptimisticLockException::optimisticLockFailed(); + } + } + /** * Deletes an entity. * @@ -269,7 +321,14 @@ class StandardEntityPersister $platform = $this->_conn->getDatabasePlatform(); $uow = $this->_em->getUnitOfWork(); + if ($versioned = $this->_class->isVersioned()) { + $versionField = $this->_class->getVersionField(); + } + foreach ($uow->getEntityChangeSet($entity) as $field => $change) { + if ($versioned && $versionField == $field) { + continue; + } $oldVal = $change[0]; $newVal = $change[1]; diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index 510e8459a..df29ad859 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -207,6 +207,9 @@ class SchemaTool $column['type'] = Type::getType($mapping['type']); $column['length'] = $mapping['length']; $column['notnull'] = ! $mapping['nullable']; + if (isset($mapping['default'])) { + $column['default'] = $mapping['default']; + } if ($class->isIdentifier($mapping['fieldName'])) { $column['primary'] = true; $options['primary'][] = $mapping['columnName']; diff --git a/tests/Doctrine/Tests/ORM/AllTests.php b/tests/Doctrine/Tests/ORM/AllTests.php index 7285051c7..7384e6fa2 100644 --- a/tests/Doctrine/Tests/ORM/AllTests.php +++ b/tests/Doctrine/Tests/ORM/AllTests.php @@ -42,6 +42,7 @@ class AllTests $suite->addTest(Mapping\AllTests::suite()); $suite->addTest(Functional\AllTests::suite()); $suite->addTest(Id\AllTests::suite()); + $suite->addTest(Locking\AllTests::suite()); return $suite; } diff --git a/tests/Doctrine/Tests/ORM/Locking/AllTests.php b/tests/Doctrine/Tests/ORM/Locking/AllTests.php new file mode 100644 index 000000000..6a497e4dd --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Locking/AllTests.php @@ -0,0 +1,30 @@ +addTestSuite('Doctrine\Tests\ORM\Locking\OptimisticTest'); + + return $suite; + } +} + +if (PHPUnit_MAIN_METHOD == 'Orm_Locking_AllTests::main') { + AllTests::main(); +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Locking/OptimisticTest.php b/tests/Doctrine/Tests/ORM/Locking/OptimisticTest.php new file mode 100644 index 000000000..cb91c2c3b --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Locking/OptimisticTest.php @@ -0,0 +1,157 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Locking\OptimisticJoinedParent'), + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Locking\OptimisticJoinedChild'), + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Locking\OptimisticStandard') + )); + } catch (\Exception $e) { + // Swallow all exceptions. We do not test the schema tool here. + } + $this->_conn = $this->_em->getConnection(); + } + + public function testJoinedInsertSetsInitialVersionValue() + { + $test = new OptimisticJoinedParent(); + $test->name = 'test'; + $this->_em->save($test); + $this->_em->flush(); + + $this->assertEquals(1, $test->version); + } + + /** + * @expectedException Doctrine\ORM\OptimisticLockException + */ + public function testJoinedFailureThrowsException() + { + $q = $this->_em->createQuery('SELECT t FROM Doctrine\Tests\ORM\Locking\OptimisticJoinedParent t WHERE t.name = :name'); + $q->setParameter('name', 'test'); + $test = $q->getSingleResult(); + + // Manually update/increment the version so we can try and save the same + // $test and make sure the exception is thrown saying the record was + // changed or updated since you read it + $this->_conn->execute('UPDATE optimistic_joined_parent SET version = ? WHERE id = ?', array(2, $test->id)); + + // Now lets change a property and try and save it again + $test->name = 'WHATT???'; + $this->_em->flush(); + } + + public function testStandardInsertSetsInitialVersionValue() + { + $test = new OptimisticStandard(); + $test->name = 'test'; + $this->_em->save($test); + $this->_em->flush(); + + $this->assertEquals(1, $test->version); + } + + /** + * @expectedException Doctrine\ORM\OptimisticLockException + */ + public function testStandardFailureThrowsException() + { + $q = $this->_em->createQuery('SELECT t FROM Doctrine\Tests\ORM\Locking\OptimisticStandard t WHERE t.name = :name'); + $q->setParameter('name', 'test'); + $test = $q->getSingleResult(); + + // Manually update/increment the version so we can try and save the same + // $test and make sure the exception is thrown saying the record was + // changed or updated since you read it + $this->_conn->execute('UPDATE optimistic_standard SET version = ? WHERE id = ?', array(2, $test->id)); + + // Now lets change a property and try and save it again + $test->name = 'WHATT???'; + $this->_em->flush(); + } +} + +/** + * @Entity + * @Table(name="optimistic_joined_parent") + * @DiscriminatorValue("parent") + * @InheritanceType("JOINED") + * @DiscriminatorColumn(name="discr", type="string") + * @SubClasses({"Doctrine\Tests\ORM\Locking\OptimisticJoinedChild"}) + */ +class OptimisticJoinedParent +{ + /** + * @Id @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + public $id; + + /** + * @Column(type="string", length=255) + */ + public $name; + + /** + * @Version @Column(type="integer") + */ + public $version; +} + +/** + * @Entity + * @Table(name="optimistic_joined_child") + * @DiscriminatorValue("child") + */ +class OptimisticJoinedChild extends OptimisticJoinedParent +{ + /** + * @Column(type="string", length=255) + */ + public $name; +} + +/** + * @Entity + * @Table(name="optimistic_standard") + */ +class OptimisticStandard +{ + /** + * @Id @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + public $id; + + /** + * @Column(type="string", length=255) + */ + public $name; + + /** + * @Version @Column(type="integer") + */ + public $version; +} \ No newline at end of file