Skip to content

Commit

Permalink
Add deletion of join table records for BelongsToMany associations
Browse files Browse the repository at this point in the history
When cascade is true we should remove rows in BelongsToMany join tables.
Ideally people will set this to false and use database constraints, but
this is a reasonable backup in case constraints aren't used.
  • Loading branch information
markstory committed Nov 2, 2013
1 parent a79b614 commit 3250a09
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 17 deletions.
62 changes: 47 additions & 15 deletions Cake/ORM/Table.php
Expand Up @@ -460,6 +460,8 @@ public function belongsTo($associated, array $options = []) {
* - targetTable: An instance of a table object to be used as the target table
* - foreignKey: The name of the field to use as foreign key, if false none
* will be used
* - dependent: Set to true if you want CakePHP to cascade deletes to the
* associated table when an entity is removed on this table.
* - conditions: array with a list of conditions to filter the join with
* - joinType: The type of join to be used (e.g. LEFT)
*
Expand Down Expand Up @@ -490,6 +492,8 @@ public function hasOne($associated, array $options = []) {
* - targetTable: An instance of a table object to be used as the target table
* - foreignKey: The name of the field to use as foreign key, if false none
* will be used
* - dependent: Set to true if you want CakePHP to cascade deletes to the
* associated table when an entity is removed on this table.
* - conditions: array with a list of conditions to filter the join with
* - sort: The order in which results for this association should be returned
* - strategy: The strategy to be used for selecting results Either 'select'
Expand Down Expand Up @@ -716,6 +720,7 @@ public function updateAll($fields, $conditions) {
*
* @param array $conditions An array of conditions, similar to those used with find()
* @return boolean Success Returns true if one or more rows are effected.
* @see Cake\ORM\Table::delete()
*/
public function deleteAll($conditions) {
$query = $this->_buildQuery();
Expand Down Expand Up @@ -939,23 +944,11 @@ public function delete(Entity $entity, array $options = []) {
return $this->_processDelete($entity, $options);
};

if ($options['atomic']) {
$success = $this->connection()->transactional($process);
} else {
$success = $process();
}
$success = $options['atomic'] ? $this->connection()->transactional($process) : $process();

if (!$success) {
return $success;
}
foreach ($this->_associations as $assoc) {
if ($assoc->dependent()) {
$this->_deleteDependent($assoc, $entity);
}
if ($assoc instanceof BelongsToMany) {
// TODO finish
}
}

$event = new Event('Model.afterDelete', $this, [
'entity' => $entity,
'options' => $options
Expand All @@ -967,6 +960,12 @@ public function delete(Entity $entity, array $options = []) {
/**
* Perform the delete operation.
*
* Will delete the entity provided. Will remove rows from any
* dependent associations, and clear out join tables for BelongsToMany associations.
*
* Setting $options['cascade'] = false will prevent associated data including
* join tables from being cleared.
*
* @param Entity $entity The entity to delete.
* @param array $options The options for the delete.
* @return boolean success
Expand All @@ -983,7 +982,21 @@ protected function _processDelete($entity, $options) {
->where($conditions)
->executeStatement();

return $statement->rowCount() > 0;
$success = $statement->rowCount() > 0;

if (!$success || !$options['cascade']) {
return $success;
}

foreach ($this->_associations as $assoc) {
// TODO Perhaps refactor the deletion into the association class?
if ($assoc->dependent()) {
$this->_deleteDependent($assoc, $entity);
} elseif ($assoc instanceof BelongsToMany) {
$this->_deletePivotRecords($assoc, $entity);
}
}
return $success;
}

/**
Expand All @@ -1010,6 +1023,25 @@ protected function _deleteDependent(Association $assoc, Entity $entity) {
}
}

/**
* Clear the data out of a BelongsToMany join table.
*
* @param Cake\ORM\Association\BelongsToMany $assoc The association to clear.
* @param Cake\ORM\Entity $entity The entity to remove pivot records for.
* @return void
*/
protected function _deletePivotRecords(BelongsToMany $assoc, Entity $entity) {
$foreignKey = $assoc->foreignKey();
$primaryKey = $this->primaryKey();
$conditions = [
$foreignKey => $entity->get($primaryKey)
];
// TODO fix multi-column primary keys.
$conditions = array_merge($conditions, $assoc->conditions());
$table = $assoc->pivot();
$table->deleteAll($conditions);
}

/**
* Calls a finder method directly and applies it to the passed query,
* if no query is passed a new one will be created and returned
Expand Down
56 changes: 54 additions & 2 deletions Cake/Test/TestCase/ORM/TableTest.php
Expand Up @@ -1599,7 +1599,28 @@ public function testDeleteDependent() {
'author_id' => $entity->id
]
]);
$this->assertNull($query->execute()->one());
$this->assertNull($query->execute()->one(), 'Should not find any rows.');
}

/**
* Test delete with dependent records and cascade = false
*
* @return void
*/
public function testDeleteDependentCascadeFalse() {
$table = TableRegistry::get('author');
$table->hasOne('article', [
'foreignKey' => 'author_id',
'dependent' => true,
]);

$query = $table->find('all')->where(['id' => 1]);
$entity = $query->first();
$result = $table->delete($entity, ['cascade' => false]);

$articles = $table->association('article')->target();
$query = $articles->find('all')->where(['author_id' => $entity->id]);
$this->assertCount(2, $query->execute(), 'Should find rows.');
}

/**
Expand All @@ -1608,7 +1629,38 @@ public function testDeleteDependent() {
* @return void
*/
public function testDeleteBelongsToMany() {
$this->markTestIncomplete('not done');
$table = TableRegistry::get('article');
$table->belongsToMany('tag', [
'foreignKey' => 'article_id',
'joinTable' => 'articles_tags'
]);
$query = $table->find('all')->where(['id' => 1]);
$entity = $query->first();
$table->delete($entity);

$pivot = $table->association('tag')->pivot();
$query = $pivot->find('all')->where(['article_id' => 1]);
$this->assertNull($query->execute()->one(), 'Should not find any rows.');
}

/**
* Test delete with belongsToMany and cascade = false
*
* @return void
*/
public function testDeleteCascadeFalseBelongsToMany() {
$table = TableRegistry::get('article');
$table->belongsToMany('tag', [
'foreignKey' => 'article_id',
'joinTable' => 'articles_tags'
]);
$query = $table->find('all')->where(['id' => 1]);
$entity = $query->first();
$table->delete($entity, ['cascade' => false]);

$pivot = $table->association('tag')->pivot();
$query = $pivot->find('all')->where(['article_id' => 1]);
$this->assertCount(2, $query->execute(), 'Should find rows.');
}

/**
Expand Down

0 comments on commit 3250a09

Please sign in to comment.