1
0
mirror of synced 2024-12-05 03:06:05 +03:00

[DDC-1654] Add support for orphanRemoval on ManyToMany associations. This only makes sense when ManyToMany is used as uni-directional OneToMany association with join table. The join column has a unique constraint on it to enforce this on the DB level, but we dont validate that this actually happens. Foreign Key constraints help prevent issues and notify developers early if they use it wrong.

This commit is contained in:
Benjamin Eberlei 2012-02-20 10:33:16 +01:00
parent 85d1707a2b
commit 68436fee75
10 changed files with 147 additions and 4 deletions

View File

@ -371,6 +371,7 @@
<xs:attribute name="index-by" type="xs:NMTOKEN" />
<xs:attribute name="inversed-by" type="xs:NMTOKEN" />
<xs:attribute name="fetch" type="orm:fetch-type" default="LAZY" />
<xs:attribute name="orphan-removal" type="xs:boolean" default="false" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>

View File

@ -1115,7 +1115,7 @@ class ClassMetadataInfo implements ClassMetadata
$mapping['targetEntity'] = ltrim($mapping['targetEntity'], '\\');
}
if ( ($mapping['type'] & (self::MANY_TO_ONE|self::MANY_TO_MANY)) > 0 &&
if ( ($mapping['type'] & self::MANY_TO_ONE) > 0 &&
isset($mapping['orphanRemoval']) &&
$mapping['orphanRemoval'] == true) {
@ -1335,6 +1335,8 @@ class ClassMetadataInfo implements ClassMetadata
}
}
$mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) ? (bool) $mapping['orphanRemoval'] : false;
if (isset($mapping['orderBy'])) {
if ( ! is_array($mapping['orderBy'])) {
throw new \InvalidArgumentException("'orderBy' is expected to be an array, not ".gettype($mapping['orderBy']));

View File

@ -412,6 +412,7 @@ class AnnotationDriver implements Driver
$mapping['inversedBy'] = $manyToManyAnnot->inversedBy;
$mapping['cascade'] = $manyToManyAnnot->cascade;
$mapping['indexBy'] = $manyToManyAnnot->indexBy;
$mapping['orphanRemoval'] = $manyToManyAnnot->orphanRemoval;
$mapping['fetch'] = $this->getFetchMode($className, $manyToManyAnnot->fetch);
if ($orderByAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OrderBy')) {

View File

@ -402,6 +402,10 @@ class XmlDriver extends AbstractFileDriver
$mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string)$manyToManyElement['fetch']);
}
if (isset($manyToManyElement['orphan-removal'])) {
$mapping['orphanRemoval'] = (bool)$manyToManyElement['orphan-removal'];
}
if (isset($manyToManyElement['mapped-by'])) {
$mapping['mappedBy'] = (string)$manyToManyElement['mapped-by'];
} else if (isset($manyToManyElement->{'join-table'})) {

View File

@ -451,6 +451,10 @@ class YamlDriver extends AbstractFileDriver
$mapping['indexBy'] = $manyToManyElement['indexBy'];
}
if (isset($manyToManyElement['orphanRemoval'])) {
$mapping['orphanRemoval'] = (bool)$manyToManyElement['orphanRemoval'];
}
$metadata->mapManyToMany($mapping);
}
}

View File

@ -35,6 +35,8 @@ final class ManyToMany implements Annotation
public $cascade;
/** @var string */
public $fetch = 'LAZY';
/** @var boolean */
public $orphanRemoval = false;
/** @var string */
public $indexBy;
}

View File

@ -388,7 +388,7 @@ final class PersistentCollection implements Collection
$this->changed();
if ($this->association !== null &&
$this->association['type'] == ClassMetadata::ONE_TO_MANY &&
$this->association['type'] & ClassMetadata::TO_MANY &&
$this->association['orphanRemoval']) {
$this->em->getUnitOfWork()->scheduleOrphanRemoval($removed);
}
@ -426,7 +426,7 @@ final class PersistentCollection implements Collection
$this->changed();
if ($this->association !== null &&
$this->association['type'] === ClassMetadata::ONE_TO_MANY &&
$this->association['type'] & ClassMetadata::TO_MANY &&
$this->association['orphanRemoval']) {
$this->em->getUnitOfWork()->scheduleOrphanRemoval($element);
}
@ -631,7 +631,7 @@ final class PersistentCollection implements Collection
$uow = $this->em->getUnitOfWork();
if ($this->association['type'] === ClassMetadata::ONE_TO_MANY && $this->association['orphanRemoval']) {
if ($this->association['type'] & ClassMetadata::TO_MANY && $this->association['orphanRemoval']) {
// we need to initialize here, as orphan removal acts like implicit cascadeRemove,
// hence for event listeners we need the objects in memory.
$this->initialize();

View File

@ -0,0 +1,103 @@
<?php
namespace Doctrine\Tests\ORM\Functional\Ticket;
/**
* @group DDC-1654
*/
class DDC1654Test extends \Doctrine\Tests\OrmFunctionalTestCase
{
public function setUp()
{
parent::setUp();
$this->setUpEntitySchema(array(
__NAMESPACE__ . '\\DDC1654Post',
__NAMESPACE__ . '\\DDC1654Comment',
));
}
public function testManyToManyRemoveFromCollectionOrphanRemoval()
{
$post = new DDC1654Post();
$post->comments[] = new DDC1654Comment();
$post->comments[] = new DDC1654Comment();
$this->_em->persist($post);
$this->_em->flush();
$post->comments->remove(0);
$post->comments->remove(1);
$this->_em->flush();
$this->_em->clear();
$comments = $this->_em->getRepository(__NAMESPACE__ . '\\DDC1654Comment')->findAll();
$this->assertEquals(0, count($comments));
}
public function testManyToManyRemoveElementFromCollectionOrphanRemoval()
{
$post = new DDC1654Post();
$post->comments[] = new DDC1654Comment();
$post->comments[] = new DDC1654Comment();
$this->_em->persist($post);
$this->_em->flush();
$post->comments->removeElement($post->comments[0]);
$post->comments->removeElement($post->comments[1]);
$this->_em->flush();
$this->_em->clear();
$comments = $this->_em->getRepository(__NAMESPACE__ . '\\DDC1654Comment')->findAll();
$this->assertEquals(0, count($comments));
}
public function testManyToManyClearCollectionOrphanRemoval()
{
$post = new DDC1654Post();
$post->comments[] = new DDC1654Comment();
$post->comments[] = new DDC1654Comment();
$this->_em->persist($post);
$this->_em->flush();
$post->comments->clear();
$this->_em->flush();
$this->_em->clear();
$comments = $this->_em->getRepository(__NAMESPACE__ . '\\DDC1654Comment')->findAll();
$this->assertEquals(0, count($comments));
}
}
/**
* @Entity
*/
class DDC1654Post
{
/**
* @Id @Column(type="integer") @GeneratedValue
*/
public $id;
/**
* @ManyToMany(targetEntity="DDC1654Comment", orphanRemoval=true,
* cascade={"persist"})
*/
public $comments = array();
}
/**
* @Entity
*/
class DDC1654Comment
{
/**
* @Id @Column(type="integer") @GeneratedValue
*/
public $id;
}

View File

@ -377,6 +377,7 @@ class ClassMetadataBuilderTest extends \Doctrine\Tests\OrmTestCase
array(
'user_id' => 'id',
),
'orphanRemoval' => false,
),
), $this->cm->associationMappings);
}

View File

@ -38,6 +38,12 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
/** Whether the database schema has already been created. */
protected static $_tablesCreated = array();
/**
* Array of entity class name to their tables that were created.
* @var array
*/
protected static $_entityTablesCreated = array();
/** List of model sets and their classes. */
protected static $_modelSets = array(
'cms' => array(
@ -235,6 +241,25 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
$this->_em->clear();
}
protected function setUpEntitySchema(array $classNames)
{
if ($this->_em === null) {
throw new \RuntimeException("EntityManager not set, you have to call parent::setUp() before invoking this method.");
}
$classes = array();
foreach ($classNames as $className) {
if ( ! isset(static::$_entityTablesCreated[$className])) {
static::$_entityTablesCreated[$className] = true;
$classes[] = $this->_em->getClassMetadata($className);
}
}
if ($classes) {
$this->_schemaTool->createSchema($classes);
}
}
/**
* Creates a connection to the test database, if there is none yet, and
* creates the necessary tables.