ORM Column now supports related table lookup

This commit is contained in:
Mojmir Fendek 2020-01-28 14:03:12 +13:00
parent 26e3b6f4e3
commit 99786dda22
3 changed files with 132 additions and 15 deletions

View File

@ -501,6 +501,16 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
* Unlike getRelationName, this is immutable and will fallback to the quoted field * Unlike getRelationName, this is immutable and will fallback to the quoted field
* name if not a relation. * name if not a relation.
* *
* Example use (simple WHERE condition on data sitting in a related table):
*
* <code>
* $columnName = null;
* $list = Page::get()
* ->applyRelation('TaxonomyTerms.ID', $columnName)
* ->where([$columnName => 'my value']);
* </code>
*
*
* @param string $field Name of field or relation to apply * @param string $field Name of field or relation to apply
* @param string &$columnName Quoted column name * @param string &$columnName Quoted column name
* @param bool $linearOnly Set to true to restrict to linear relations only. Set this * @param bool $linearOnly Set to true to restrict to linear relations only. Set this

View File

@ -4,6 +4,7 @@ namespace SilverStripe\ORM;
use SilverStripe\Core\ClassInfo; use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\Connect\Query; use SilverStripe\ORM\Connect\Query;
use SilverStripe\ORM\Queries\SQLConditionGroup; use SilverStripe\ORM\Queries\SQLConditionGroup;
@ -21,6 +22,8 @@ use InvalidArgumentException;
class DataQuery class DataQuery
{ {
use Extensible;
/** /**
* @var string * @var string
*/ */
@ -930,7 +933,7 @@ class DataQuery
$joinExpression = "{$foreignKeyIDColumn} = {$localIDColumn}"; $joinExpression = "{$foreignKeyIDColumn} = {$localIDColumn}";
} }
$this->query->addLeftJoin( $this->query->addLeftJoin(
$foreignTable, $this->augmentTable($foreignClass, $foreignTable),
$joinExpression, $joinExpression,
$foreignTableAliased $foreignTableAliased
); );
@ -944,7 +947,7 @@ class DataQuery
if ($ancestorTable !== $foreignTable) { if ($ancestorTable !== $foreignTable) {
$ancestorTableAliased = $foreignPrefix . $ancestorTable; $ancestorTableAliased = $foreignPrefix . $ancestorTable;
$this->query->addLeftJoin( $this->query->addLeftJoin(
$ancestorTable, $this->augmentTable($ancestor, $ancestorTable),
"\"{$foreignTableAliased}\".\"ID\" = \"{$ancestorTableAliased}\".\"ID\"", "\"{$foreignTableAliased}\".\"ID\" = \"{$ancestorTableAliased}\".\"ID\"",
$ancestorTableAliased $ancestorTableAliased
); );
@ -990,7 +993,7 @@ class DataQuery
$foreignIDColumn = $schema->sqlColumnForField($foreignBaseClass, 'ID', $foreignPrefix); $foreignIDColumn = $schema->sqlColumnForField($foreignBaseClass, 'ID', $foreignPrefix);
$localColumn = $schema->sqlColumnForField($localClass, "{$localField}ID", $localPrefix); $localColumn = $schema->sqlColumnForField($localClass, "{$localField}ID", $localPrefix);
$this->query->addLeftJoin( $this->query->addLeftJoin(
$foreignBaseTable, $this->augmentTable($foreignClass, $foreignBaseTable),
"{$foreignIDColumn} = {$localColumn}", "{$foreignIDColumn} = {$localColumn}",
$foreignPrefix . $foreignBaseTable $foreignPrefix . $foreignBaseTable
); );
@ -1005,7 +1008,7 @@ class DataQuery
if ($ancestorTable !== $foreignBaseTable) { if ($ancestorTable !== $foreignBaseTable) {
$ancestorTableAliased = $foreignPrefix . $ancestorTable; $ancestorTableAliased = $foreignPrefix . $ancestorTable;
$this->query->addLeftJoin( $this->query->addLeftJoin(
$ancestorTable, $this->augmentTable($ancestor, $ancestorTable),
"{$foreignIDColumn} = \"{$ancestorTableAliased}\".\"ID\"", "{$foreignIDColumn} = \"{$ancestorTableAliased}\".\"ID\"",
$ancestorTableAliased $ancestorTableAliased
); );
@ -1039,7 +1042,13 @@ class DataQuery
$schema = DataObject::getSchema(); $schema = DataObject::getSchema();
if (class_exists($relationClassOrTable)) { if (class_exists($relationClassOrTable)) {
$relationClassOrTable = $schema->tableName($relationClassOrTable); // class is provided
$relationTable = $schema->tableName($relationClassOrTable);
$relationTableAugmented = $this->augmentTable($relationClassOrTable, $relationTable);
} else {
// table is provided
$relationTable = $relationClassOrTable;
$relationTableAugmented = $relationClassOrTable;
} }
// Check if already joined to component alias (skip join table for the check) // Check if already joined to component alias (skip join table for the check)
@ -1051,10 +1060,10 @@ class DataQuery
} }
// Join parent class to join table // Join parent class to join table
$relationAliasedTable = $componentPrefix . $relationClassOrTable; $relationAliasedTable = $componentPrefix . $relationTable;
$parentIDColumn = $schema->sqlColumnForField($parentClass, 'ID', $parentPrefix); $parentIDColumn = $schema->sqlColumnForField($parentClass, 'ID', $parentPrefix);
$this->query->addLeftJoin( $this->query->addLeftJoin(
$relationClassOrTable, $relationTableAugmented,
"\"{$relationAliasedTable}\".\"{$parentField}\" = {$parentIDColumn}", "\"{$relationAliasedTable}\".\"{$parentField}\" = {$parentIDColumn}",
$relationAliasedTable $relationAliasedTable
); );
@ -1062,7 +1071,7 @@ class DataQuery
// Join on base table of component class // Join on base table of component class
$componentIDColumn = $schema->sqlColumnForField($componentBaseClass, 'ID', $componentPrefix); $componentIDColumn = $schema->sqlColumnForField($componentBaseClass, 'ID', $componentPrefix);
$this->query->addLeftJoin( $this->query->addLeftJoin(
$componentBaseTable, $this->augmentTable($componentBaseClass, $componentBaseTable),
"\"{$relationAliasedTable}\".\"{$componentField}\" = {$componentIDColumn}", "\"{$relationAliasedTable}\".\"{$componentField}\" = {$componentIDColumn}",
$componentAliasedTable $componentAliasedTable
); );
@ -1076,7 +1085,7 @@ class DataQuery
if ($ancestorTable !== $componentBaseTable) { if ($ancestorTable !== $componentBaseTable) {
$ancestorTableAliased = $componentPrefix . $ancestorTable; $ancestorTableAliased = $componentPrefix . $ancestorTable;
$this->query->addLeftJoin( $this->query->addLeftJoin(
$ancestorTable, $this->augmentTable($ancestor, $ancestorTable),
"{$componentIDColumn} = \"{$ancestorTableAliased}\".\"ID\"", "{$componentIDColumn} = \"{$ancestorTableAliased}\".\"ID\"",
$ancestorTableAliased $ancestorTableAliased
); );
@ -1084,6 +1093,22 @@ class DataQuery
} }
} }
/**
* Use this extension point to alter the table name
* useful for versioning for example
*
* @param $class
* @param $table
* @return mixed
*/
protected function augmentTable($class, $table)
{
$augmented = $table;
$this->invokeWithExtensions('augmentJoinTable', $class, $table, $augmented);
return $augmented;
}
/** /**
* Removes the result of query from this query. * Removes the result of query from this query.
* *
@ -1142,16 +1167,47 @@ class DataQuery
/** /**
* Query the given field column from the database and return as an array. * Query the given field column from the database and return as an array.
* querying DB columns of related tables is supported but you need to make sure that the related table
* is already available in join
*
* @see DataList::applyRelation()
*
* example use:
*
* <code>
* column("MyTable"."Title")
*
* or
*
* $columnName = null;
* Category::get()
* ->applyRelation('Products.Title', $columnName)
* ->column($columnName);
* </code>
* *
* @param string $field See {@link expressionForField()}. * @param string $field See {@link expressionForField()}.
* @return array List of column values for the specified column * @return array List of column values for the specified column
* @throws InvalidArgumentException
*/ */
public function column($field = 'ID') public function column($field = 'ID')
{ {
$fieldExpression = $this->expressionForField($field); $fieldExpression = $this->expressionForField($field);
$query = $this->getFinalisedQuery(array($field)); $query = $this->getFinalisedQuery([$field]);
$originalSelect = $query->getSelect(); $originalSelect = $query->getSelect();
$query->setSelect(array()); $query->setSelect([]);
// field wasn't recognised as a valid field from the table class hierarchy
// check if the field is in format "<table_name>"."<column_name>"
// if that's the case we may want to query related table
if (!$fieldExpression) {
if (!$this->validateColumnField($field, $query)) {
throw new InvalidArgumentException('Invalid column name ' . $field);
}
$fieldExpression = $field;
$field = null;
}
$query->selectField($fieldExpression, $field); $query->selectField($fieldExpression, $field);
$this->ensureSelectContainsOrderbyColumns($query, $originalSelect); $this->ensureSelectContainsOrderbyColumns($query, $originalSelect);
@ -1257,4 +1313,22 @@ class DataQuery
$this->dataQueryManipulators[] = $manipulator; $this->dataQueryManipulators[] = $manipulator;
return $this; return $this;
} }
private function validateColumnField($field, SQLSelect $query)
{
// standard column - nothing to process here
if (strpos($field, '.') === false) {
return false;
}
$fieldData = explode('.', $field);
$tablePrefix = str_replace('"', '', $fieldData[0]);
// check if related table is available
if (!$query->isJoinedTo($tablePrefix)) {
return false;
}
return true;
}
} }

View File

@ -4,24 +4,24 @@ namespace SilverStripe\ORM\Tests;
use InvalidArgumentException; use InvalidArgumentException;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\InjectorNotFoundException; use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataQuery; use SilverStripe\ORM\DataQuery;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\ORM\Filterable; use SilverStripe\ORM\Filterable;
use SilverStripe\ORM\Filters\ExactMatchFilter; use SilverStripe\ORM\Filters\ExactMatchFilter;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\Tests\DataObjectTest\Fixture;
use SilverStripe\ORM\Tests\DataObjectTest\Bracket; use SilverStripe\ORM\Tests\DataObjectTest\Bracket;
use SilverStripe\ORM\Tests\DataObjectTest\EquipmentCompany; use SilverStripe\ORM\Tests\DataObjectTest\EquipmentCompany;
use SilverStripe\ORM\Tests\DataObjectTest\Fan; use SilverStripe\ORM\Tests\DataObjectTest\Fan;
use SilverStripe\ORM\Tests\DataObjectTest\Fixture;
use SilverStripe\ORM\Tests\DataObjectTest\Player; use SilverStripe\ORM\Tests\DataObjectTest\Player;
use SilverStripe\ORM\Tests\DataObjectTest\Sortable; use SilverStripe\ORM\Tests\DataObjectTest\Sortable;
use SilverStripe\ORM\Tests\DataObjectTest\Staff;
use SilverStripe\ORM\Tests\DataObjectTest\SubTeam; use SilverStripe\ORM\Tests\DataObjectTest\SubTeam;
use SilverStripe\ORM\Tests\DataObjectTest\Team; use SilverStripe\ORM\Tests\DataObjectTest\Team;
use SilverStripe\ORM\Tests\DataObjectTest\TeamComment; use SilverStripe\ORM\Tests\DataObjectTest\TeamComment;
use SilverStripe\ORM\Tests\DataObjectTest\ValidatedObject; use SilverStripe\ORM\Tests\DataObjectTest\ValidatedObject;
use SilverStripe\ORM\Tests\DataObjectTest\Staff; use SilverStripe\ORM\Tests\ManyManyListTest\Category;
/** /**
* @skipUpgrade * @skipUpgrade
@ -1835,4 +1835,37 @@ class DataListTest extends SapphireTest
$list->column("Title") $list->column("Title")
); );
} }
public function testColumnFailureInvalidColumn()
{
$this->expectException(InvalidArgumentException::class);
Category::get()->column('ObviouslyInvalidColumn');
}
public function testColumnFailureInvalidTable()
{
$this->expectException(InvalidArgumentException::class);
$columnName = null;
Category::get()
->applyRelation('Products.ID', $columnName)
->column('"ObviouslyInvalidTable"."ID"');
}
public function testColumnFromRelatedTable()
{
$columnName = null;
$productTitles = Category::get()
->applyRelation('Products.Title', $columnName)
->column($columnName);
$productTitles = array_diff($productTitles, [null]);
sort($productTitles);
$this->assertEquals([
'Product A',
'Product B',
], $productTitles);
}
} }