ENHANCEMENT: First cut of running SearchContext through DataList/DataQuery. Note that the eventual goal is probably to ditch SearchContext entirely.

This commit is contained in:
Sam Minnee 2011-03-21 21:37:55 +13:00
parent f83abe416c
commit c615c4eb91
15 changed files with 129 additions and 152 deletions

View File

@ -353,6 +353,85 @@ class DataQuery {
}
}
/**
* Traverse the relationship fields, and add the table
* mappings to the query object state. This has to be called
* in any overloaded {@link SearchFilter->apply()} methods manually.
*
* @param $relation The array/dot-syntax relation to follow
* @return The model class of the related item
*/
function applyRelation($relation) {
// NO-OP
if(!$relation) return $this->dataClass;
if(is_string($relation)) $relation = explode(".", $relation);
$modelClass = $this->dataClass;
foreach($relation as $rel) {
$model = singleton($modelClass);
if ($component = $model->has_one($rel)) {
if(!$this->query->isJoinedTo($component)) {
$foreignKey = $model->getReverseAssociation($component);
$this->query->leftJoin($component, "\"$component\".\"ID\" = \"{$modelClass}\".\"{$foreignKey}ID\"");
/**
* add join clause to the component's ancestry classes so that the search filter could search on its
* ancester fields.
*/
$ancestry = ClassInfo::ancestry($component, true);
if(!empty($ancestry)){
$ancestry = array_reverse($ancestry);
foreach($ancestry as $ancestor){
if($ancestor != $component){
$this->query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
$component=$ancestor;
}
}
}
}
$modelClass = $component;
} elseif ($component = $model->has_many($rel)) {
if(!$this->query->isJoinedTo($component)) {
$ancestry = $model->getClassAncestry();
$foreignKey = $model->getRemoteJoinField($rel);
$this->query->leftJoin($component, "\"$component\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\"");
/**
* add join clause to the component's ancestry classes so that the search filter could search on its
* ancestor fields.
*/
$ancestry = ClassInfo::ancestry($component, true);
if(!empty($ancestry)){
$ancestry = array_reverse($ancestry);
foreach($ancestry as $ancestor){
if($ancestor != $component){
$this->query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
$component=$ancestor;
}
}
}
}
$modelClass = $component;
} elseif ($component = $model->many_many($rel)) {
list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component;
$parentBaseClass = ClassInfo::baseDataClass($parentClass);
$componentBaseClass = ClassInfo::baseDataClass($componentClass);
$this->query->innerJoin($relationTable, "\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\"");
$this->query->leftJoin($componentBaseClass, "\"$relationTable\".\"$componentField\" = \"$componentBaseClass\".\"ID\"");
if(ClassInfo::hasTable($componentClass)) {
$this->query->leftJoin($componentClass, "\"$relationTable\".\"$componentField\" = \"$componentClass\".\"ID\"");
}
$modelClass = $componentClass;
}
}
return $modelClass;
}
/**
* Select the given fields from the given table
*/

View File

@ -115,19 +115,17 @@ class SearchContext extends Object {
* @return SQLQuery
*/
public function getQuery($searchParams, $sort = false, $limit = false, $existingQuery = null) {
$model = singleton($this->modelClass);
if($existingQuery) {
if(!($existingQuery instanceof DataList)) throw new InvalidArgumentException("existingQuery must be DataList");
if($existingQuery->dataClass() != $this->modelClass) throw new InvalidArgumentException("existingQuery's dataClass is " . $existingQuery->dataClass() . ", $this->modelClass expected.");
$query = $existingQuery;
} else {
$query = $model->extendedSQL();
$query = DataList::create($this->modelClass);
}
$SQL_limit = Convert::raw2sql($limit);
$query->limit($SQL_limit);
$SQL_sort = (!empty($sort)) ? Convert::raw2sql($sort) : singleton($this->modelClass)->stat('default_sort');
$query->orderby($SQL_sort);
$query->limit($limit);
$query->sort($sort);
// hack to work with $searchParems when it's an Object
$searchParamArray = array();
@ -143,15 +141,12 @@ class SearchContext extends Object {
$filter->setModel($this->modelClass);
$filter->setValue($value);
if(! $filter->isEmpty()) {
$filter->apply($query);
$filter->apply($query->dataQuery());
}
}
}
$query->connective = $this->connective;
$query->distinct = true;
$model->extend('augmentSQL', $query);
if($this->connective != "AND") throw new Exception("SearchContext connective '$this->connective' not supported after ORM-rewrite.");
return $query;
}
@ -169,17 +164,8 @@ class SearchContext extends Object {
public function getResults($searchParams, $sort = false, $limit = false) {
$searchParams = array_filter($searchParams, array($this,'clearEmptySearchFields'));
$query = $this->getQuery($searchParams, $sort, $limit);
// use if a raw SQL query is needed
$results = new DataObjectSet();
foreach($query->execute() as $row) {
$className = $row['RecordClassName'];
$results->push(new $className($row));
}
return $results;
//
//return DataObject::get($this->modelClass, $query->getFilter(), "", "", $limit);
// getQuery actually returns a DataList
return $this->getQuery($searchParams, $sort, $limit);
}
/**

View File

@ -23,9 +23,9 @@ class EndsWithFilter extends SearchFilter {
*
* @return unknown
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
$query->where($this->getDbName() . " LIKE '%" . Convert::raw2sql($this->getValue()) . "'");
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$query->filter($this->getDbName() . " LIKE '%" . Convert::raw2sql($this->getValue()) . "'");
}
public function isEmpty() {

View File

@ -20,9 +20,9 @@ class ExactMatchFilter extends SearchFilter {
*
* @return unknown
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
return $query->where(sprintf(
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->filter(sprintf(
"%s = '%s'",
$this->getDbName(),
Convert::raw2sql($this->getValue())

View File

@ -14,9 +14,8 @@
*/
class ExactMatchMultiFilter extends SearchFilter {
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
// hack
// PREVIOUS $values = explode(',',$this->getValue());
$values = array();
@ -40,7 +39,7 @@ class ExactMatchMultiFilter extends SearchFilter {
}
$SQL_valueStr = "'" . implode("','", $values) . "'";
return $query->where(sprintf(
return $query->filter(sprintf(
"%s IN (%s)",
$this->getDbName(),
$SQL_valueStr

View File

@ -27,7 +27,7 @@
*/
class FulltextFilter extends SearchFilter {
public function apply(SQLQuery $query) {
public function apply(DataQuery $query) {
$query->where(sprintf(
"MATCH (%s) AGAINST ('%s')",
$this->getDbName(),

View File

@ -12,9 +12,9 @@ class GreaterThanFilter extends SearchFilter {
/**
* @return $query
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
return $query->where(sprintf(
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->filter(sprintf(
"%s > '%s'",
$this->getDbName(),
Convert::raw2sql($this->getDbFormattedValue())

View File

@ -12,9 +12,9 @@ class LessThanFilter extends SearchFilter {
/**
* @return $query
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
return $query->where(sprintf(
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->filter(sprintf(
"%s < '%s'",
$this->getDbName(),
Convert::raw2sql($this->getDbFormattedValue())

View File

@ -7,8 +7,8 @@
*/
class NegationFilter extends SearchFilter {
public function apply(SQLQuery $query) {
return $query->where(sprintf(
public function apply(DataQuery $query) {
return $query->filter(sprintf(
"%s != '%s'",
$this->getDbName(),
Convert::raw2sql($this->getValue())

View File

@ -12,9 +12,9 @@
*/
class PartialMatchFilter extends SearchFilter {
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
return $query->where(sprintf(
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->filter(sprintf(
"%s LIKE '%%%s%%'",
$this->getDbName(),
Convert::raw2sql($this->getValue())

View File

@ -150,95 +150,6 @@ abstract class SearchFilter extends Object {
return $dbField->RAW();
}
/**
* Traverse the relationship fields, and add the table
* mappings to the query object state. This has to be called
* in any overloaded {@link SearchFilter->apply()} methods manually.
*
* @todo try to make this implicitly triggered so it doesn't have to be manually called in child filters
* @param SQLQuery $query
* @return SQLQuery
*/
function applyRelation($query) {
if (is_array($this->relation)) {
foreach($this->relation as $rel) {
$model = singleton($this->model);
if ($component = $model->has_one($rel)) {
if(!$query->isJoinedTo($component)) {
$foreignKey = $model->getReverseAssociation($component);
$query->leftJoin($component, "\"$component\".\"ID\" = \"{$this->model}\".\"{$foreignKey}ID\"");
/**
* add join clause to the component's ancestry classes so that the search filter could search on its
* ancester fields.
*/
$ancestry = ClassInfo::ancestry($component, true);
if(!empty($ancestry)){
$ancestry = array_reverse($ancestry);
foreach($ancestry as $ancestor){
if($ancestor != $component){
$query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
$component=$ancestor;
}
}
}
}
$this->model = $component;
} elseif ($component = $model->has_many($rel)) {
if(!$query->isJoinedTo($component)) {
$ancestry = $model->getClassAncestry();
$foreignKey = $model->getRemoteJoinField($rel);
$query->leftJoin($component, "\"$component\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\"");
/**
* add join clause to the component's ancestry classes so that the search filter could search on its
* ancestor fields.
*/
$ancestry = ClassInfo::ancestry($component, true);
if(!empty($ancestry)){
$ancestry = array_reverse($ancestry);
foreach($ancestry as $ancestor){
if($ancestor != $component){
$query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
$component=$ancestor;
}
}
}
}
$this->model = $component;
} elseif ($component = $model->many_many($rel)) {
list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component;
$parentBaseClass = ClassInfo::baseDataClass($parentClass);
$componentBaseClass = ClassInfo::baseDataClass($componentClass);
$query->innerJoin($relationTable, "\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\"");
$query->leftJoin($componentBaseClass, "\"$relationTable\".\"$componentField\" = \"$componentBaseClass\".\"ID\"");
if(ClassInfo::hasTable($componentClass)) {
$query->leftJoin($componentClass, "\"$relationTable\".\"$componentField\" = \"$componentClass\".\"ID\"");
}
$this->model = $componentClass;
// Experimental support for user-defined relationships via a "(relName)Query" method
// This will likely be dropped in 2.4 for a system that makes use of Lazy Data Lists.
} elseif($model->hasMethod($rel.'Query')) {
// Get the query representing the join - it should have "$ID" in the filter
$newQuery = $model->{"{$rel}Query"}();
if($newQuery) {
// Get the table to join to
//DATABASE ABSTRACTION: I don't think we need this line anymore:
$newModel = str_replace('`','',array_shift($newQuery->from));
// Get the filter to use on the join
$ancestry = $model->getClassAncestry();
$newFilter = "(" . str_replace('$ID', "\"{$ancestry[0]}\".\"ID\"" , implode(") AND (", $newQuery->where) ) . ")";
$query->leftJoin($newModel, $newFilter);
$this->model = $newModel;
} else {
$this->name = "NULL";
return;
}
}
}
}
return $query;
}
/**
* Apply filter criteria to a SQL query.
@ -246,7 +157,7 @@ abstract class SearchFilter extends Object {
* @param SQLQuery $query
* @return SQLQuery
*/
abstract public function apply(SQLQuery $query);
abstract public function apply(DataQuery $query);
/**
* Determines if a field has a value,

View File

@ -23,9 +23,9 @@ class StartsWithFilter extends SearchFilter {
*
* @return unknown
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
$query->where($this->getDbName() . " LIKE '" . Convert::raw2sql($this->getValue()) . "%'");
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$query->filter($this->getDbName() . " LIKE '" . Convert::raw2sql($this->getValue()) . "%'");
}
public function isEmpty() {

View File

@ -14,8 +14,8 @@
*/
class StartsWithMultiFilter extends SearchFilter {
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$values = explode(',', $this->getValue());
foreach($values as $value) {
@ -25,7 +25,7 @@ class StartsWithMultiFilter extends SearchFilter {
);
}
return $query->where(implode(" OR ", $matches));
return $query->filter(implode(" OR ", $matches));
}
public function isEmpty() {

View File

@ -12,8 +12,8 @@
*/
class SubstringFilter extends SearchFilter {
public function apply(SQLQuery $query) {
return $query->where(sprintf(
public function apply(DataQuery $query) {
return $query->filter(sprintf(
"LOCATE('%s', %s) != 0",
Convert::raw2sql($this->getValue()),
$this->getDbName()

View File

@ -65,6 +65,7 @@ class SQLQueryTest extends SapphireTest {
}
function testSelectWithPredicateFilters() {
/* this is no longer part of this
$query = new SQLQuery();
$query->select(array("Name"))->from("SQLQueryTest_DO");
@ -77,6 +78,7 @@ class SQLQueryTest extends SapphireTest {
$match->apply($query);
$this->assertEquals("SELECT Name FROM SQLQueryTest_DO WHERE (\"SQLQueryTest_DO\".\"Name\" = 'Value') AND (\"SQLQueryTest_DO\".\"Meta\" LIKE '%Value%')", $query->sql());
*/
}
function testSelectWithLimitClause() {