From bb2a68caec1480a8196d7f3187db105194d29b91 Mon Sep 17 00:00:00 2001 From: nate Date: Mon, 26 Mar 2007 17:16:43 +0000 Subject: [PATCH] Rewriting SQL generation for self-joins git-svn-id: https://svn.cakephp.org/repo/branches/1.2.x.x@4679 3807eeeb-6ff5-0310-8944-8be069107fe0 --- cake/libs/model/datasources/dbo_source.php | 43 +-- cake/libs/model/model.php | 20 +- .../model/datasources/dbo_source.test.php | 253 +++++++++++++++++- cake/tests/cases/libs/model/model.test.php | 14 +- 4 files changed, 286 insertions(+), 44 deletions(-) diff --git a/cake/libs/model/datasources/dbo_source.php b/cake/libs/model/datasources/dbo_source.php index 18df1cf87..83fd044c7 100644 --- a/cake/libs/model/datasources/dbo_source.php +++ b/cake/libs/model/datasources/dbo_source.php @@ -824,17 +824,32 @@ class DboSource extends DataSource { */ function generateSelfAssociationQuery(&$model, &$linkModel, $type, $association = null, $assocData = array(), &$queryData, $external = false, &$resultSet) { $alias = $association; + if (empty($alias) && !empty($linkModel)) { + $alias = $linkModel->name; + } + if (!isset($queryData['selfJoin'])) { $queryData['selfJoin'] = array(); - $sql = 'SELECT ' . join(', ', $this->fields($model, null, $queryData['fields'])); - if($this->__bypass === false){ - $sql .= ', '; - $sql .= join(', ', $this->fields($linkModel, $alias, '')); + $self = array( + 'fields' => $this->fields($model, null, $queryData['fields']), + 'joins' => array(array( + 'table' => $this->fullTableName($linkModel), + 'alias' => $alias, + 'type' => 'LEFT', + 'conditions' => array($model->escapeField($assocData['foreignKey']) => '{$__cakeIdentifier[' . "{$alias}.{$linkModel->primaryKey}" . ']__$}') + )), + 'table' => $this->fullTableName($model), + 'alias' => $model->name, + 'limit' => $queryData['limit'], + 'offset' => $queryData['offset'], + 'conditions'=> $queryData['conditions'], + 'order' => $queryData['order'] + ); + + if($this->__bypass === false) { + $self['fields'] = am($self['fields'], $this->fields($linkModel, $alias, '')); } - $sql .= ' FROM ' . $this->fullTableName($model) . ' ' . $this->alias . $this->name($model->name); - $sql .= ' LEFT JOIN ' . $this->fullTableName($linkModel) . ' ' . $this->alias . $this->name($alias); - $sql .= ' ON ' . $this->name($model->name) . '.' . $this->name($assocData['foreignKey']); - $sql .= ' = ' . $this->name($alias) . '.' . $this->name($linkModel->primaryKey); + $sql = $this->buildStatement($self, $model); if (!in_array($sql, $queryData['selfJoin'])) { $queryData['selfJoin'][] = $sql; @@ -843,18 +858,10 @@ class DboSource extends DataSource { } elseif (isset($linkModel)) { return $this->generateAssociationQuery($model, $linkModel, $type, $association, $assocData, $queryData, $external, $resultSet); } else { + $result = $queryData['selfJoin'][0]; if (isset($this->__assocJoins)) { - $replace = ', '; - $replace .= join(', ', $this->__assocJoins['fields']); - $replace .= ' FROM'; - } else { - $replace = 'FROM'; + $result = preg_replace('/FROM/', ', ' . join(', ', $this->__assocJoins['fields']) . ' FROM', $result); } - $sql = $queryData['selfJoin'][0]; - $sql .= ' ' . join(' ', $queryData['joins']); - $sql .= $this->conditions($queryData['conditions']) . ' ' . $this->order($queryData['order']); - $sql .= ' ' . $this->limit($queryData['limit'], $queryData['offset']); - $result = preg_replace('/FROM/', $replace, $sql); return $result; } } diff --git a/cake/libs/model/model.php b/cake/libs/model/model.php index 8b52a31b6..0b5f4c359 100644 --- a/cake/libs/model/model.php +++ b/cake/libs/model/model.php @@ -305,6 +305,14 @@ class Model extends Overloadable { */ var $__associations = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'); +/** + * Holds association data to be reverted to + * + * @var array + * @access protected + */ + var $__backAssociation = array(); + /** * The last inserted ID of the data that this model created * @@ -478,7 +486,7 @@ class Model extends Overloadable { $return = $db->query($method, $params, $this); if (!PHP5) { - if (isset($this->__backAssociation)) { + if (!empty($this->__backAssociation)) { $this->__resetAssociations(); } } @@ -571,7 +579,7 @@ class Model extends Overloadable { */ function unbindModel($params, $reset = true) { foreach($params as $assoc => $models) { - if($reset === true){ + if($reset === true) { $this->__backAssociation[$assoc] = $this->{$assoc}; } @@ -1246,9 +1254,9 @@ class Model extends Overloadable { * @access protected */ function _deleteDependent($id, $cascade) { - if (isset($this->__backAssociation)) { + if (!empty($this->__backAssociation)) { $savedAssociatons = $this->__backAssociation; - unset ($this->__backAssociation); + $this->__backAssociation = array(); } foreach(am($this->hasMany, $this->hasOne) as $assoc => $data) { if ($data['dependent'] === true && $cascade === true) { @@ -1439,7 +1447,7 @@ class Model extends Overloadable { } $return = $this->afterFind($results, true); - if (isset($this->__backAssociation)) { + if (!empty($this->__backAssociation)) { $this->__resetAssociations(); } @@ -1460,7 +1468,7 @@ class Model extends Overloadable { } } - unset ($this->__backAssociation); + $this->__backAssociation = array(); return true; } /** diff --git a/cake/tests/cases/libs/model/datasources/dbo_source.test.php b/cake/tests/cases/libs/model/datasources/dbo_source.test.php index 1d805654a..c86958fee 100644 --- a/cake/tests/cases/libs/model/datasources/dbo_source.test.php +++ b/cake/tests/cases/libs/model/datasources/dbo_source.test.php @@ -233,6 +233,216 @@ class TestModel7 extends Model { return $this->_tableInfo; } } + +class Level extends Model { + var $name = 'Level'; + var $table = 'level'; + var $useTable = false; + + var $hasMany = array( + 'Group'=> array( + 'className' => 'Group' + ), + 'User' => array( + 'className' => 'User' + ) + ); + + function loadInfo() { + if (!isset($this->_tableInfo)) { + $this->_tableInfo = new Set(array( + array('name' => 'id', 'type' => 'integer', 'null' => false, 'default' => null, 'length' => '10'), + array('name' => 'name', 'type' => 'string', 'null' => true, 'default' => null, 'length' => '20') + )); + } + return $this->_tableInfo; + } +} + +class Group extends Model { + var $name = 'Group'; + var $table = 'group'; + var $useTable = false; + + var $belongsTo = array( + 'Level' => array( + 'className' => 'Level' + ) + ); + + var $hasMany = array( + 'Category'=> array( + 'className' => 'Category' + ), + 'User'=> array( + 'className' => 'User' + ) + ); + + function loadInfo() { + if (!isset($this->_tableInfo)) { + $this->_tableInfo = new Set(array( + array('name' => 'id', 'type' => 'integer', 'null' => false, 'default' => null, 'length' => '10'), + array('name' => 'level_id', 'type' => 'integer', 'null' => false, 'default' => '0', 'length' => '10'), + array('name' => 'name', 'type' => 'string', 'null' => true, 'default' => null, 'length' => '20') + )); + } + return $this->_tableInfo; + } +} + +class User extends Model { + var $name = 'User'; + var $table = 'user'; + var $useTable = false; + + var $belongsTo = array( + 'Group' => array( + 'className' => 'Group' + ), + 'Level' => array( + 'className' => 'Level' + ) + ); + + var $hasMany = array( + 'Article' => array( + 'className' => 'Article' + ), + ); + + function loadInfo() { + if (!isset($this->_tableInfo)) { + $this->_tableInfo = new Set(array( + array('name' => 'id', 'type' => 'integer', 'null' => false, 'default' => null, 'length' => '10'), + array('name' => 'group_id', 'type' => 'integer', 'null' => false, 'default' => '0', 'length' => '10'), + array('name' => 'level_id', 'type' => 'integer', 'null' => false, 'default' => '0', 'length' => '10'), + array('name' => 'name', 'type' => 'string', 'null' => true, 'default' => null, 'length' => '20') + )); + } + return $this->_tableInfo; + } +} + +class Category extends Model { + var $name = 'Category'; + var $table = 'category'; + var $useTable = false; + + var $belongsTo = array( + 'Group' => array( + 'className' => 'Group', + 'foreignKey' => 'group_id' + ), + 'ParentCat' => array( + 'className' => 'Category', + 'foreignKey' => 'parent_id' + ) + ); + + var $hasMany = array( + 'ChildCat' => array( + 'className' => 'Category', + 'foreignKey' => 'parent_id' + ), + 'Article' => array( + 'className' => 'Article', + 'order'=>'Article.published_date DESC', + 'foreignKey' => 'category_id', + 'limit'=>'3') + ); + + function loadInfo() { + if (!isset($this->_tableInfo)) { + $this->_tableInfo = new Set(array( + array('name' => 'id', 'type' => 'integer', 'null' => false, 'default' => '', 'length' => '10'), + array('name' => 'group_id', 'type' => 'integer', 'null' => false, 'default' => '', 'length' => '10'), + array('name' => 'parent_id', 'type' => 'integer', 'null' => false, 'default' => '', 'length' => '10'), + array('name' => 'name', 'type' => 'string', 'null' => false, 'default' => '', 'length' => '255'), + array('name' => 'icon', 'type' => 'string', 'null' => false, 'default' => '', 'length' => '255'), + array('name' => 'description', 'text' => 'string', 'null' => false, 'default' => '', 'length' => null) + )); + } + return $this->_tableInfo; + } +} + +class Article extends Model { + var $name = 'Article'; + var $table = 'article'; + var $useTable = false; + + var $belongsTo = array( + 'Category' => array( + 'className' => 'Category' + ), + 'User' => array( + 'className' => 'User' + ) + ); + + var $hasOne = array( + 'Featured' => array( + 'className' => 'Featured' + ) + ); + + function loadInfo() { + if (!isset($this->_tableInfo)) { + $this->_tableInfo = new Set(array( + array('name' => 'id', 'type' => 'integer', 'null' => false, 'default' => '', 'length' => '10'), + array('name' => 'category_id', 'type' => 'integer', 'null' => false, 'default' => '0', 'length' => '10'), + array('name' => 'user_id', 'type' => 'integer', 'null' => false, 'default' => '0', 'length' => '10'), + array('name' => 'rate_count', 'type' => 'integer', 'null' => false, 'default' => '0', 'length' => '10'), + array('name' => 'rate_sum', 'type' => 'integer', 'null' => false, 'default' => '0', 'length' => '10'), + array('name' => 'viewed', 'type' => 'integer', 'null' => false, 'default' => '0', 'length' => '10'), + array('name' => 'version', 'type' => 'string', 'null' => true, 'default' => '', 'length' => '45'), + array('name' => 'title', 'type' => 'string', 'null' => false, 'default' => '', 'length' => '200'), + array('name' => 'intro', 'text' => 'string', 'null' => true, 'default' => '', 'length' => null), + array('name' => 'comments', 'type' => 'integer', 'null' => false, 'default' => '0', 'length' => '4'), + array('name' => 'body', 'text' => 'string', 'null' => true, 'default' => '', 'length' => null), + array('name' => 'isdraft', 'type' => 'boolean', 'null' => false, 'default' => '0', 'length' => '1'), + array('name' => 'allow_comments', 'type' => 'boolean', 'null' => false, 'default' => '1', 'length' => '1'), + array('name' => 'moderate_comments', 'type' => 'boolean', 'null' => false, 'default' => '1', 'length' => '1'), + array('name' => 'published', 'type' => 'boolean', 'null' => false, 'default' => '0', 'length' => '1'), + array('name' => 'multipage', 'type' => 'boolean', 'null' => false, 'default' => '0', 'length' => '1'), + array('name' => 'published_date', 'type' => 'datetime', 'null' => true, 'default' => '', 'length' => null), + array('name' => 'created', 'type' => 'datetime', 'null' => false, 'default' => '0000-00-00 00:00:00', 'length' => null), + array('name' => 'modified', 'type' => 'datetime', 'null' => false, 'default' => '0000-00-00 00:00:00', 'length' => null) + )); + } + return $this->_tableInfo; + } +} + +class Featured extends Model { + + var $name = 'Featured'; + var $table = 'article'; + var $useTable = false; + + var $belongsTo = array( + 'Article' => array( + 'className' => 'Article' + ), + 'Category' => array( + 'Artiucle' => 'Category' + ) + ); + + function loadInfo() { + if (!isset($this->_tableInfo)) { + $this->_tableInfo = new Set(array( + array('name' => 'id', 'type' => 'integer', 'null' => false, 'default' => null, 'length' => '10'), + array('name' => 'article_id', 'type' => 'integer', 'null' => false, 'default' => '0', 'length' => '10'), + array('name' => 'category_id', 'type' => 'integer', 'null' => false, 'default' => '0', 'length' => '10'), + array('name' => 'name', 'type' => 'string', 'null' => true, 'default' => null, 'length' => '20') + )); + } + return $this->_tableInfo; + } +} + /** * Short description for class. * @@ -276,6 +486,34 @@ class DboSourceTest extends UnitTestCase { } function testGenerateAssociationQuerySelfJoin() { + $this->model = new Article(); + $this->_buildRelatedModels($this->model); + $this->_buildRelatedModels($this->model->Category); + $this->model->Category->ChildCat = new Category(); + $this->model->Category->ParentCat = new Category(); + + $queryData = array(); + + foreach($this->model->Category->__associations as $type) { + foreach($this->model->Category->{$type} as $assoc => $assocData) { + $linkModel =& $this->model->Category->{$assoc}; + $external = isset($assocData['external']); + + if ($this->model->Category->name == $linkModel->name && $type != 'hasAndBelongsToMany' && $type != 'hasMany') { + $result = $this->db->generateSelfAssociationQuery($this->model->Category, $linkModel, $type, $assoc, $assocData, $queryData, $external, $null); + $this->assertTrue($result); + } else { + if ($this->model->Category->useDbConfig == $linkModel->useDbConfig) { + $result = $this->db->generateAssociationQuery($this->model->Category, $linkModel, $type, $assoc, $assocData, $queryData, $external, $null); + $this->assertTrue($result); + } + } + } + } + + $query = $this->db->generateAssociationQuery($model, $null, null, null, null, $queryData, false, $null); + $this->assertPattern('/^SELECT\s+(.+)FROM(.+)LEFT\s+JOIN\s+`category`\s+AS\s+`ParentCat`\s+ON\s+`Category`\.`parent_id`\s+=\s+`ParentCat`\.`id`\s+WHERE/', $query); + $this->model = new TestModel4(); $this->model->loadInfo(); $this->_buildRelatedModels($this->model); @@ -291,18 +529,9 @@ class DboSourceTest extends UnitTestCase { $this->assertTrue($result); $this->assertPattern('/^SELECT\s+`TestModel4`\.`id`, `TestModel4`\.`name`, `TestModel4`\.`created`, `TestModel4`\.`updated`, `TestModel4Parent`\.`id`, `TestModel4Parent`\.`name`, `TestModel4Parent`\.`created`, `TestModel4Parent`\.`updated`\s+/', $queryData['selfJoin'][0]); - $this->assertPattern('/FROM\s+/', $queryData['selfJoin'][0]); - $expected = 'SELECT '; - $expected .= '`TestModel4`.`id`, `TestModel4`.`name`, `TestModel4`.`created`, `TestModel4`.`updated`, `TestModel4Parent`.`id`, `TestModel4Parent`.`name`, `TestModel4Parent`.`created`, `TestModel4Parent`.`updated`'; - $expected .= ' FROM '; - $expected .= '`test_model4` AS `TestModel4`'; - $expected .= ' LEFT JOIN '; - $expected .= '`test_model4` AS `TestModel4Parent`'; - $expected .= ' ON '; - $expected .= '`TestModel4`.`parent_id` = `TestModel4Parent`.`id`'; - - $this->assertEqual($queryData['selfJoin'][0], $expected); - + $this->assertPattern('/FROM\s+`test_model4` AS `TestModel4`\s+LEFT JOIN\s+`test_model4` AS `TestModel4Parent`/', $queryData['selfJoin'][0]); + $this->assertPattern('/\s+ON\s+`TestModel4`.`parent_id` = `TestModel4Parent`.`id`\s+WHERE\s+1 = 1\s*$/', $queryData['selfJoin'][0]); + $result = $this->db->generateAssociationQuery($this->model, $null, null, null, null, $queryData, false, $null); $this->assertPattern('/^SELECT\s+`TestModel4`\.`id`, `TestModel4`\.`name`, `TestModel4`\.`created`, `TestModel4`\.`updated`, `TestModel4Parent`\.`id`, `TestModel4Parent`\.`name`, `TestModel4Parent`\.`created`, `TestModel4Parent`\.`updated`\s+/', $result); $this->assertPattern('/FROM\s+`test_model4` AS `TestModel4`\s+LEFT JOIN\s+`test_model4` AS `TestModel4Parent`/', $result); diff --git a/cake/tests/cases/libs/model/model.test.php b/cake/tests/cases/libs/model/model.test.php index 5a0fa8647..71ee896d1 100644 --- a/cake/tests/cases/libs/model/model.test.php +++ b/cake/tests/cases/libs/model/model.test.php @@ -264,7 +264,7 @@ class ModelTest extends CakeTestCase { $result = $this->model->bindModel(array('hasMany' => array('Comment'))); $this->assertTrue($result); - + $result = $this->model->findAll(null, 'User.id, User.user'); $expected = array( array ( 'User' => array ( 'id' => '1', 'user' => 'mariano'), 'Comment' => array( @@ -282,11 +282,11 @@ class ModelTest extends CakeTestCase { )) ); $this->assertEqual($result, $expected); - + + $this->model->__resetAssociations(); $result = $this->model->hasMany; - $expected = array(); - $this->assertEqual($result, $expected); - + $this->assertEqual($result, array()); + $result = $this->model->bindModel(array('hasMany' => array('Comment')), false); $this->assertTrue($result); @@ -346,7 +346,6 @@ class ModelTest extends CakeTestCase { ); $this->assertEqual($result, $expected); - /* $result = $this->model->unbindModel(array('hasMany' => array('Comment')), false); $this->assertTrue($result); @@ -362,8 +361,7 @@ class ModelTest extends CakeTestCase { $result = $this->model->hasMany; $expected = array(); $this->assertEqual($result, $expected); - */ - + $result = $this->model->bindModel(array('hasMany' => array('Comment' => array('className' => 'Comment', 'conditions' => 'Comment.published = \'Y\'') ))); $this->assertTrue($result);