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()