Merge pull request #2520 from bar/2.5-dbosource-read

Document and try to order part of ```DboSource```.
This commit is contained in:
Mark Story 2013-12-21 14:04:29 -08:00
commit 0c4a5c59e4
2 changed files with 146 additions and 90 deletions

View file

@ -318,10 +318,10 @@ class DataSource extends Object {
* *
* @param string $query Query string needing replacements done. * @param string $query Query string needing replacements done.
* @param array $data Array of data with values that will be inserted in placeholders. * @param array $data Array of data with values that will be inserted in placeholders.
* @param string $association Name of association model being replaced * @param string $association Name of association model being replaced.
* @param Model $Model Model instance * @param Model $Model Model instance.
* @param array $stack * @param array $stack
* @return string String of query data with placeholders replaced. * @return mixed String of query data with placeholders replaced, or false on failure.
*/ */
public function insertQueryData($query, $data, $association, Model $Model, $stack) { public function insertQueryData($query, $data, $association, Model $Model, $stack) {
$keys = array('{$__cakeID__$}', '{$__cakeForeignKey__$}'); $keys = array('{$__cakeID__$}', '{$__cakeForeignKey__$}');

View file

@ -1042,29 +1042,34 @@ class DboSource extends DataSource {
} }
if ($recursive !== null) { if ($recursive !== null) {
$_recursive = $Model->recursive; $modelRecursive = $Model->recursive;
$Model->recursive = $recursive; $Model->recursive = $recursive;
} }
$bypass = false;
if (!empty($queryData['fields'])) { if (!empty($queryData['fields'])) {
$bypass = true; $noAssocFields = true;
$queryData['fields'] = $this->fields($Model, null, $queryData['fields']); $queryData['fields'] = $this->fields($Model, null, $queryData['fields']);
} else { } else {
$noAssocFields = false;
$queryData['fields'] = $this->fields($Model); $queryData['fields'] = $this->fields($Model);
} }
$_associations = $Model->associations(); if ($Model->recursive === -1) {
// Primary model data only, no joins.
$associations = array();
if ($Model->recursive == -1) { } else {
$_associations = array(); $associations = $Model->associations();
} elseif ($Model->recursive === 0) {
unset($_associations[2], $_associations[3]); if ($Model->recursive === 0) {
// Primary model data and its domain.
unset($associations[2], $associations[3]);
}
} }
// Generate hasOne and belongsTo associations inside $queryData // Generate hasOne and belongsTo associations inside $queryData
$linkedModels = array(); $linkedModels = array();
foreach ($_associations as $type) { foreach ($associations as $type) {
if ($type !== 'hasOne' && $type !== 'belongsTo') { if ($type !== 'hasOne' && $type !== 'belongsTo') {
continue; continue;
} }
@ -1076,7 +1081,7 @@ class DboSource extends DataSource {
continue; continue;
} }
if ($bypass) { if ($noAssocFields) {
$assocData['fields'] = false; $assocData['fields'] = false;
} }
@ -1113,7 +1118,7 @@ class DboSource extends DataSource {
$joined[$Model->alias] = (array)Hash::extract($queryData['joins'], '{n}.alias'); $joined[$Model->alias] = (array)Hash::extract($queryData['joins'], '{n}.alias');
} }
foreach ($_associations as $type) { foreach ($associations as $type) {
foreach ($Model->{$type} as $assoc => $assocData) { foreach ($Model->{$type} as $assoc => $assocData) {
$LinkModel = $Model->{$assoc}; $LinkModel = $Model->{$assoc};
@ -1142,7 +1147,7 @@ class DboSource extends DataSource {
} }
if ($recursive !== null) { if ($recursive !== null) {
$Model->recursive = $_recursive; $Model->recursive = $modelRecursive;
} }
return $resultSet; return $resultSet;
@ -1153,17 +1158,17 @@ class DboSource extends DataSource {
* *
* The primary model is always excluded, because the filtering is later done by Model::_filterResults(). * The primary model is always excluded, because the filtering is later done by Model::_filterResults().
* *
* @param array $results Reference of resultset to be filtered * @param array $resultSet Reference of resultset to be filtered.
* @param Model $Model Instance of model to operate against * @param Model $Model Instance of model to operate against.
* @param array $filtered List of classes already filtered, to be skipped * @param array $filtered List of classes already filtered, to be skipped.
* @return array Array of results that have been filtered through $Model->afterFind * @return array Array of results that have been filtered through $Model->afterFind.
*/ */
protected function _filterResults(&$results, Model $Model, $filtered = array()) { protected function _filterResults(&$resultSet, Model $Model, $filtered = array()) {
if (!is_array($results)) { if (!is_array($resultSet)) {
return array(); return array();
} }
$current = reset($results); $current = reset($resultSet);
if (!is_array($current)) { if (!is_array($current)) {
return array(); return array();
} }
@ -1179,12 +1184,12 @@ class DboSource extends DataSource {
$LinkedModel = $Model->{$className}; $LinkedModel = $Model->{$className};
$filtering[] = $className; $filtering[] = $className;
foreach ($results as $key => &$result) { foreach ($resultSet as $key => &$result) {
$data = $LinkedModel->afterFind(array(array($className => $result[$className])), false); $data = $LinkedModel->afterFind(array(array($className => $result[$className])), false);
if (isset($data[0][$className])) { if (isset($data[0][$className])) {
$result[$className] = $data[0][$className]; $result[$className] = $data[0][$className];
} else { } else {
unset($results[$key]); unset($resultSet[$key]);
} }
} }
} }
@ -1193,7 +1198,15 @@ class DboSource extends DataSource {
} }
/** /**
* Queries associations. Used to fetch results on recursive models. * Queries associations.
*
* Used to fetch results on recursive models.
*
* - 'hasMany' associations with no limit set:
* Fetch, filter and merge is done recursively for every level.
*
* - 'hasAndBelongsToMany' associations:
* Fetch and filter is done unaffected by the (recursive) level set.
* *
* @param Model $Model Primary Model object. * @param Model $Model Primary Model object.
* @param Model $LinkModel Linked model object. * @param Model $LinkModel Linked model object.
@ -1214,8 +1227,8 @@ class DboSource extends DataSource {
unset($stack['_joined']); unset($stack['_joined']);
} }
$query = $this->generateAssociationQuery($Model, $LinkModel, $type, $association, $assocData, $queryData, $external); $queryTemplate = $this->generateAssociationQuery($Model, $LinkModel, $type, $association, $assocData, $queryData, $external);
if (empty($query)) { if (empty($queryTemplate)) {
return; return;
} }
@ -1224,19 +1237,22 @@ class DboSource extends DataSource {
} }
if ($type === 'hasMany' && empty($assocData['limit']) && !empty($assocData['foreignKey'])) { if ($type === 'hasMany' && empty($assocData['limit']) && !empty($assocData['foreignKey'])) {
$ins = $fetch = array(); // 'hasMany' associations with no limit set.
foreach ($resultSet as &$result) {
if ($in = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack)) { $assocIds = array();
$ins[] = $in; foreach ($resultSet as $result) {
$assocIds[] = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack);
} }
$assocIds = array_filter($assocIds);
// Fetch
$assocResultSet = array();
if (!empty($assocIds)) {
$assocResultSet = $this->_fetchHasMany($Model, $queryTemplate, $assocIds);
} }
if (!empty($ins)) { // Recursively query associations
$ins = array_unique($ins); if ($recursive > 0 && !empty($assocResultSet) && is_array($assocResultSet)) {
$fetch = $this->fetchAssociated($Model, $query, $ins);
}
if ($recursive > 0 && !empty($fetch) && is_array($fetch)) {
foreach ($LinkModel->associations() as $type1) { foreach ($LinkModel->associations() as $type1) {
foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) { foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) {
$DeepModel = $LinkModel->{$assoc1}; $DeepModel = $LinkModel->{$assoc1};
@ -1245,89 +1261,88 @@ class DboSource extends DataSource {
$db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource(); $db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource();
$db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $fetch, $recursive - 1, $tmpStack); $db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $assocResultSet, $recursive - 1, $tmpStack);
} }
} }
} }
// Filter
if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') {
$this->_filterResults($fetch, $Model); $this->_filterResults($assocResultSet, $Model);
} }
return $this->_mergeHasMany($resultSet, $fetch, $association, $Model); // Merge
return $this->_mergeHasMany($resultSet, $assocResultSet, $association, $Model);
} elseif ($type === 'hasAndBelongsToMany') { } elseif ($type === 'hasAndBelongsToMany') {
$ins = $fetch = array(); // 'hasAndBelongsToMany' associations.
foreach ($resultSet as &$result) {
if ($in = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack)) { $assocIds = array();
$ins[] = $in; foreach ($resultSet as $result) {
$assocIds[] = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack);
} }
$assocIds = array_filter($assocIds);
// Fetch
$assocResultSet = array();
if (!empty($assocIds)) {
$assocResultSet = $this->_fetchHasAndBelongsToMany($Model, $queryTemplate, $assocIds, $association);
} }
if (!empty($ins)) { $habtmAssocData = $Model->hasAndBelongsToMany[$association];
$ins = array_unique($ins); $foreignKey = $habtmAssocData['foreignKey'];
if (count($ins) > 1) { $joinKeys = array($foreignKey, $habtmAssocData['associationForeignKey']);
$query = str_replace('{$__cakeID__$}', '(' . implode(', ', $ins) . ')', $query); list($with, $habtmFields) = $Model->joinModel($habtmAssocData['with'], $joinKeys);
$query = str_replace('= (', 'IN (', $query);
} else {
$query = str_replace('{$__cakeID__$}', $ins[0], $query);
}
$query = str_replace(' WHERE 1 = 1', '', $query);
}
$foreignKey = $Model->hasAndBelongsToMany[$association]['foreignKey'];
$joinKeys = array($foreignKey, $Model->hasAndBelongsToMany[$association]['associationForeignKey']);
list($with, $habtmFields) = $Model->joinModel($Model->hasAndBelongsToMany[$association]['with'], $joinKeys);
$habtmFieldsCount = count($habtmFields); $habtmFieldsCount = count($habtmFields);
$q = $this->insertQueryData($query, null, $association, $Model, $stack);
if ($q !== false) {
$fetch = $this->fetchAll($q, $Model->cacheQueries);
} else {
$fetch = null;
}
// Filter
if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') {
$this->_filterResults($fetch, $Model); $this->_filterResults($assocResultSet, $Model);
} }
} }
$modelAlias = $Model->alias; $modelAlias = $Model->alias;
$primaryKey = $Model->primaryKey; $primaryKey = $Model->primaryKey;
foreach ($resultSet as &$row) {
if ($type !== 'hasAndBelongsToMany') {
$q = $this->insertQueryData($query, $row, $association, $Model, $stack);
$fetch = null;
if ($q !== false) {
$joinedData = array();
if (($type === 'belongsTo' || $type === 'hasOne') && isset($row[$LinkModel->alias], $joined[$Model->alias]) && in_array($LinkModel->alias, $joined[$Model->alias])) {
$joinedData = Hash::filter($row[$LinkModel->alias]);
if (!empty($joinedData)) {
$fetch[0] = array($LinkModel->alias => $row[$LinkModel->alias]);
}
} else {
$fetch = $this->fetchAll($q, $Model->cacheQueries);
}
}
}
$selfJoin = ($Model->name === $LinkModel->name); $selfJoin = ($Model->name === $LinkModel->name);
if (!empty($fetch) && is_array($fetch)) { foreach ($resultSet as &$row) {
if ($type === 'hasOne' || $type === 'belongsTo' || $type === 'hasMany') {
$assocResultSet = array();
if (
($type === 'hasOne' || $type === 'belongsTo') &&
isset($row[$LinkModel->alias], $joined[$Model->alias]) &&
in_array($LinkModel->alias, $joined[$Model->alias])
) {
$joinedData = Hash::filter($row[$LinkModel->alias]);
if (!empty($joinedData)) {
$assocResultSet[0] = array($LinkModel->alias => $row[$LinkModel->alias]);
}
} else {
$query = $this->insertQueryData($queryTemplate, $row, $association, $Model, $stack);
if ($query !== false) {
$assocResultSet = $this->fetchAll($query, $Model->cacheQueries);
}
}
}
if (!empty($assocResultSet) && is_array($assocResultSet)) {
if ($recursive > 0) { if ($recursive > 0) {
foreach ($LinkModel->associations() as $type1) { foreach ($LinkModel->associations() as $type1) {
foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) { foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) {
$DeepModel = $LinkModel->{$assoc1}; $DeepModel = $LinkModel->{$assoc1};
if ($type1 === 'belongsTo' || ($DeepModel->alias === $modelAlias && $type === 'belongsTo') || ($DeepModel->alias !== $modelAlias)) { if (
$type1 === 'belongsTo' ||
($type === 'belongsTo' && $DeepModel->alias === $modelAlias) ||
($DeepModel->alias !== $modelAlias)
) {
$tmpStack = $stack; $tmpStack = $stack;
$tmpStack[] = $assoc1; $tmpStack[] = $assoc1;
$db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource(); $db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource();
$db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $fetch, $recursive - 1, $tmpStack); $db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $assocResultSet, $recursive - 1, $tmpStack);
} }
} }
} }
@ -1335,8 +1350,7 @@ class DboSource extends DataSource {
if ($type === 'hasAndBelongsToMany') { if ($type === 'hasAndBelongsToMany') {
$merge = array(); $merge = array();
foreach ($assocResultSet as $data) {
foreach ($fetch as $data) {
if (isset($data[$with]) && $data[$with][$foreignKey] === $row[$modelAlias][$primaryKey]) { if (isset($data[$with]) && $data[$with][$foreignKey] === $row[$modelAlias][$primaryKey]) {
if ($habtmFieldsCount <= 2) { if ($habtmFieldsCount <= 2) {
unset($data[$with]); unset($data[$with]);
@ -1344,13 +1358,14 @@ class DboSource extends DataSource {
$merge[] = $data; $merge[] = $data;
} }
} }
if (empty($merge) && !isset($row[$association])) { if (empty($merge) && !isset($row[$association])) {
$row[$association] = $merge; $row[$association] = $merge;
} else { } else {
$this->_mergeAssociation($row, $merge, $association, $type); $this->_mergeAssociation($row, $merge, $association, $type);
} }
} else { } else {
$this->_mergeAssociation($row, $fetch, $association, $type, $selfJoin); $this->_mergeAssociation($row, $assocResultSet, $association, $type, $selfJoin);
} }
if ($type !== 'hasAndBelongsToMany' && isset($row[$association])) { if ($type !== 'hasAndBelongsToMany' && isset($row[$association])) {
@ -1365,18 +1380,59 @@ class DboSource extends DataSource {
} }
/** /**
* A more efficient way to fetch associations. * Fetch 'hasMany' associations.
*
* This is just a proxy to maintain BC.
*
* @param Model $Model Primary model object.
* @param string $query Association query template.
* @param array $ids Array of IDs of associated records.
* @return array Association results.
* @see DboSource::_fetchHasMany()
*/
public function fetchAssociated(Model $Model, $query, $ids) {
return $this->_fetchHasMany($Model, $query, $ids);
}
/**
* Fetch 'hasMany' associations.
* *
* @param Model $Model Primary model object. * @param Model $Model Primary model object.
* @param string $query Association query template. * @param string $query Association query template.
* @param array $ids Array of IDs of associated records. * @param array $ids Array of IDs of associated records.
* @return array Association results. * @return array Association results.
*/ */
public function fetchAssociated(Model $Model, $query, $ids) { protected function _fetchHasMany(Model $Model, $query, $ids) {
$ids = array_unique($ids);
$query = str_replace('{$__cakeID__$}', implode(', ', $ids), $query); $query = str_replace('{$__cakeID__$}', implode(', ', $ids), $query);
if (count($ids) > 1) { if (count($ids) > 1) {
$query = str_replace('= (', 'IN (', $query); $query = str_replace('= (', 'IN (', $query);
} }
return $this->fetchAll($query, $Model->cacheQueries);
}
/**
* Fetch 'hasAndBelongsToMany' associations.
*
* @param Model $Model Primary model object.
* @param string $query Association query.
* @param array $ids Array of IDs of associated records.
* @param string $association Association name.
* @return array Association results.
*/
protected function _fetchHasAndBelongsToMany(Model $Model, $query, $ids, $association) {
$ids = array_unique($ids);
if (count($ids) > 1) {
$query = str_replace('{$__cakeID__$}', '(' . implode(', ', $ids) . ')', $query);
$query = str_replace('= (', 'IN (', $query);
} else {
$query = str_replace('{$__cakeID__$}', $ids[0], $query);
}
$query = str_replace(' WHERE 1 = 1', '', $query);
return $this->fetchAll($query, $Model->cacheQueries); return $this->fetchAll($query, $Model->cacheQueries);
} }