diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index 19aacd237..6216d75dc 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -88,6 +88,7 @@ + diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadata.php b/lib/Doctrine/ORM/Mapping/ClassMetadata.php index 2465f1c3a..4ec0ede53 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadata.php @@ -334,6 +334,10 @@ class ClassMetadata extends ClassMetadataInfo $serialized[] = 'namedQueries'; } + if ($this->isReadOnly) { + $serialized[] = 'isReadOnly'; + } + return $serialized; } diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 7db39621d..a10d8c866 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -484,6 +484,17 @@ class ClassMetadataInfo implements ClassMetadata */ public $reflClass; + /** + * Is this entity marked as "read-only"? + * + * That means it is never considered for change-tracking in the UnitOfWork. It is a very helpful performance + * optimization for entities that are immutable, either in your domain or through the relation database + * (coming from a view, or a history table for example). + * + * @var bool + */ + public $isReadOnly = false; + /** * Initializes a new ClassMetadata instance that will hold the object-relational mapping * metadata of the class with the given name. @@ -1818,4 +1829,14 @@ class ClassMetadataInfo implements ClassMetadata { $this->versionField = $versionField; } + + /** + * Mark this class as read only, no change tracking is applied to it. + * + * @return void + */ + public function markReadOnly() + { + $this->isReadOnly = true; + } } diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index f976d0aec..b74fc860e 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -132,6 +132,10 @@ class AnnotationDriver implements Driver if (isset($classAnnotations['Doctrine\ORM\Mapping\Entity'])) { $entityAnnot = $classAnnotations['Doctrine\ORM\Mapping\Entity']; $metadata->setCustomRepositoryClass($entityAnnot->repositoryClass); + + if ($entityAnnot->readOnly) { + $metadata->markReadOnly(); + } } else if (isset($classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'])) { $metadata->isMappedSuperclass = true; } else { diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index 31e712d42..84d2cd1c8 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -1,7 +1,5 @@ setCustomRepositoryClass( isset($xmlRoot['repository-class']) ? (string)$xmlRoot['repository-class'] : null ); + if (isset($xmlRoot['read-only']) && $xmlRoot['read-only'] == "true") { + $metadata->markReadOnly(); + } } else if ($xmlRoot->getName() == 'mapped-superclass') { $metadata->isMappedSuperclass = true; } else { diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 75bfeec74..97617a64d 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -49,6 +49,9 @@ class YamlDriver extends AbstractFileDriver $metadata->setCustomRepositoryClass( isset($element['repositoryClass']) ? $element['repositoryClass'] : null ); + if (isset($element['readOnly']) && $element['readOnly'] == true) { + $metadata->markReadOnly(); + } } else if ($element['type'] == 'mappedSuperclass') { $metadata->isMappedSuperclass = true; } else { diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 79c946f87..945815a69 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -514,9 +514,9 @@ class UnitOfWork implements PropertyChangedListener $class = $this->em->getClassMetadata($className); // Skip class if instances are read-only - //if ($class->isReadOnly) { - // continue; - //} + if ($class->isReadOnly) { + continue; + } // If change tracking is explicit or happens through notification, then only compute // changes on entities of that type that are explicitly marked for synchronization. diff --git a/tests/Doctrine/Tests/ORM/Functional/AllTests.php b/tests/Doctrine/Tests/ORM/Functional/AllTests.php index cac410281..4dd2b0644 100644 --- a/tests/Doctrine/Tests/ORM/Functional/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Functional/AllTests.php @@ -59,6 +59,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Functional\PostgreSQLIdentityStrategyTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ExtraLazyCollectionTest'); $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ClearEventTest'); + $suite->addTestSuite('Doctrine\Tests\ORM\Functional\ReadOnlyTest'); $suite->addTest(Locking\AllTests::suite()); $suite->addTest(Ticket\AllTests::suite()); diff --git a/tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php b/tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php new file mode 100644 index 000000000..8a5819956 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/ReadOnlyTest.php @@ -0,0 +1,61 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\ReadOnlyEntity'), + )); + } + + public function testReadOnlyEntityNeverChangeTracked() + { + $readOnly = new ReadOnlyEntity("Test1", 1234); + $this->_em->persist($readOnly); + $this->_em->flush(); + + $readOnly->name = "Test2"; + $readOnly->number = 4321; + + $this->_em->flush(); + $this->_em->clear(); + + $dbReadOnly = $this->_em->find('Doctrine\Tests\ORM\Functional\ReadOnlyEntity', $readOnly->id); + $this->assertEquals("Test1", $dbReadOnly->name); + $this->assertEquals(1234, $dbReadOnly->number); + } +} + +/** + * @Entity(readOnly=true) + */ +class ReadOnlyEntity +{ + /** + * @Id @GeneratedValue @Column(type="integer") + * @var int + */ + public $id; + /** @column(type="string") */ + public $name; + /** @Column(type="integer") */ + public $number; + + public function __construct($name, $number) + { + $this->name = $name; + $this->number = $number; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php index 9f3bc357e..8f57280df 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php @@ -30,6 +30,8 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $cm->setCustomRepositoryClass("UserRepository"); $cm->setDiscriminatorColumn(array('name' => 'disc', 'type' => 'integer')); $cm->mapOneToOne(array('fieldName' => 'phonenumbers', 'targetEntity' => 'Bar', 'mappedBy' => 'foo')); + $cm->markReadOnly(); + $cm->addNamedQuery(array('name' => 'dql', 'query' => 'foo')); $this->assertEquals(1, count($cm->associationMappings)); $serialized = serialize($cm); @@ -51,6 +53,8 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $this->assertTrue($oneOneMapping['fetch'] == ClassMetadata::FETCH_LAZY); $this->assertEquals('phonenumbers', $oneOneMapping['fieldName']); $this->assertEquals('Doctrine\Tests\Models\CMS\Bar', $oneOneMapping['targetEntity']); + $this->assertTrue($cm->isReadOnly); + $this->assertEquals(array('dql' => 'foo'), $cm->namedQueries); } public function testFieldIsNullable()