Commit cc3ea569 authored by jwage's avatar jwage

[2.0] Initial version of optimistic locking with integer version columns

parent c9b03282
......@@ -746,8 +746,9 @@ class Connection
if ($this->_transactionNestingLevel == 1) {
$this->_transactionNestingLevel = 0;
$this->_conn->rollback();
} else {
--$this->_transactionNestingLevel;
}
--$this->_transactionNestingLevel;
return true;
}
......
......@@ -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;
}
......
......@@ -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();
}
/**
......
......@@ -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';
......
......@@ -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.
*
......
......@@ -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()) {
......
......@@ -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;
......
......@@ -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
<?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;
/**
* EntityManagerException
*
* @author Konsta Vesterinen <kvesteri@cc.hut.fi>
* @author Roman Borschel <roman@code-factory.org>
* @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
......@@ -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);
}
}
......
......@@ -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];
......
......@@ -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'];
......
......@@ -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;
}
......
<?php
namespace Doctrine\Tests\ORM\Locking;
if (!defined('PHPUnit_MAIN_METHOD')) {
define('PHPUnit_MAIN_METHOD', 'Orm_Locking_AllTests::main');
}
require_once __DIR__ . '/../../TestInit.php';
class AllTests
{
public static function main()
{
\PHPUnit_TextUI_TestRunner::run(self::suite());
}
public static function suite()
{
$suite = new \Doctrine\Tests\DoctrineTestSuite('Doctrine Orm Locking');
$suite->addTestSuite('Doctrine\Tests\ORM\Locking\OptimisticTest');
return $suite;
}
}
if (PHPUnit_MAIN_METHOD == 'Orm_Locking_AllTests::main') {
AllTests::main();
}
\ No newline at end of file
<?php
namespace Doctrine\Tests\ORM\Locking;
use Doctrine\ORM\Locking;
use Doctrine\Tests\Mocks\MetadataDriverMock;
use Doctrine\Tests\Mocks\DatabasePlatformMock;
use Doctrine\Tests\Mocks\EntityManagerMock;
use Doctrine\Tests\Mocks\ConnectionMock;
use Doctrine\Tests\Mocks\DriverMock;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Common\EventManager;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\Tests\TestUtil;
require_once __DIR__ . '/../../TestInit.php';
class OptimisticTest extends \Doctrine\Tests\OrmFunctionalTestCase
{
protected function setUp()
{
parent::setUp();
try {
$this->_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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment