Merge pull request #9386 from silverstripe-terraformers/feature/orm-column

ORM bugfix and enhancement
This commit is contained in:
Steve Boyd 2020-02-11 15:56:03 +13:00 committed by GitHub
commit 8dcaed25f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 128 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
* 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 &$columnName Quoted column name
* @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\Convert;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\Connect\Query;
use SilverStripe\ORM\Queries\SQLConditionGroup;
@ -21,6 +22,8 @@ use InvalidArgumentException;
class DataQuery
{
use Extensible;
/**
* @var string
*/
@ -930,7 +933,7 @@ class DataQuery
$joinExpression = "{$foreignKeyIDColumn} = {$localIDColumn}";
}
$this->query->addLeftJoin(
$foreignTable,
$this->getJoinTableName($foreignClass, $foreignTable),
$joinExpression,
$foreignTableAliased
);
@ -944,7 +947,7 @@ class DataQuery
if ($ancestorTable !== $foreignTable) {
$ancestorTableAliased = $foreignPrefix . $ancestorTable;
$this->query->addLeftJoin(
$ancestorTable,
$this->getJoinTableName($ancestor, $ancestorTable),
"\"{$foreignTableAliased}\".\"ID\" = \"{$ancestorTableAliased}\".\"ID\"",
$ancestorTableAliased
);
@ -990,7 +993,7 @@ class DataQuery
$foreignIDColumn = $schema->sqlColumnForField($foreignBaseClass, 'ID', $foreignPrefix);
$localColumn = $schema->sqlColumnForField($localClass, "{$localField}ID", $localPrefix);
$this->query->addLeftJoin(
$foreignBaseTable,
$this->getJoinTableName($foreignClass, $foreignBaseTable),
"{$foreignIDColumn} = {$localColumn}",
$foreignPrefix . $foreignBaseTable
);
@ -1005,7 +1008,7 @@ class DataQuery
if ($ancestorTable !== $foreignBaseTable) {
$ancestorTableAliased = $foreignPrefix . $ancestorTable;
$this->query->addLeftJoin(
$ancestorTable,
$this->getJoinTableName($ancestor, $ancestorTable),
"{$foreignIDColumn} = \"{$ancestorTableAliased}\".\"ID\"",
$ancestorTableAliased
);
@ -1039,7 +1042,13 @@ class DataQuery
$schema = DataObject::getSchema();
if (class_exists($relationClassOrTable)) {
$relationClassOrTable = $schema->tableName($relationClassOrTable);
// class is provided
$relationTable = $schema->tableName($relationClassOrTable);
$relationTableUpdated = $this->getJoinTableName($relationClassOrTable, $relationTable);
} else {
// table is provided
$relationTable = $relationClassOrTable;
$relationTableUpdated = $relationClassOrTable;
}
// 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
$relationAliasedTable = $componentPrefix . $relationClassOrTable;
$relationAliasedTable = $componentPrefix . $relationTable;
$parentIDColumn = $schema->sqlColumnForField($parentClass, 'ID', $parentPrefix);
$this->query->addLeftJoin(
$relationClassOrTable,
$relationTableUpdated,
"\"{$relationAliasedTable}\".\"{$parentField}\" = {$parentIDColumn}",
$relationAliasedTable
);
@ -1062,7 +1071,7 @@ class DataQuery
// Join on base table of component class
$componentIDColumn = $schema->sqlColumnForField($componentBaseClass, 'ID', $componentPrefix);
$this->query->addLeftJoin(
$componentBaseTable,
$this->getJoinTableName($componentBaseClass, $componentBaseTable),
"\"{$relationAliasedTable}\".\"{$componentField}\" = {$componentIDColumn}",
$componentAliasedTable
);
@ -1076,7 +1085,7 @@ class DataQuery
if ($ancestorTable !== $componentBaseTable) {
$ancestorTableAliased = $componentPrefix . $ancestorTable;
$this->query->addLeftJoin(
$ancestorTable,
$this->getJoinTableName($ancestor, $ancestorTable),
"{$componentIDColumn} = \"{$ancestorTableAliased}\".\"ID\"",
$ancestorTableAliased
);
@ -1142,16 +1151,47 @@ class DataQuery
/**
* 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()}.
* @return array List of column values for the specified column
* @throws InvalidArgumentException
*/
public function column($field = 'ID')
{
$fieldExpression = $this->expressionForField($field);
$query = $this->getFinalisedQuery(array($field));
$query = $this->getFinalisedQuery([$field]);
$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);
$this->ensureSelectContainsOrderbyColumns($query, $originalSelect);
@ -1257,4 +1297,34 @@ class DataQuery
$this->dataQueryManipulators[] = $manipulator;
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
return $query->isJoinedTo($tablePrefix);
}
/**
* Use this extension point to alter the table name
* useful for versioning for example
*
* @param $class
* @param $table
* @return mixed
*/
private function getJoinTableName($class, $table)
{
$updated = $table;
$this->invokeWithExtensions('updateJoinTableName', $class, $table, $updated);
return $updated;
}
}

View File

@ -4,24 +4,24 @@ namespace SilverStripe\ORM\Tests;
use InvalidArgumentException;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\InjectorNotFoundException;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataQuery;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\Filterable;
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\EquipmentCompany;
use SilverStripe\ORM\Tests\DataObjectTest\Fan;
use SilverStripe\ORM\Tests\DataObjectTest\Fixture;
use SilverStripe\ORM\Tests\DataObjectTest\Player;
use SilverStripe\ORM\Tests\DataObjectTest\Sortable;
use SilverStripe\ORM\Tests\DataObjectTest\Staff;
use SilverStripe\ORM\Tests\DataObjectTest\SubTeam;
use SilverStripe\ORM\Tests\DataObjectTest\Team;
use SilverStripe\ORM\Tests\DataObjectTest\TeamComment;
use SilverStripe\ORM\Tests\DataObjectTest\ValidatedObject;
use SilverStripe\ORM\Tests\DataObjectTest\Staff;
use SilverStripe\ORM\Tests\ManyManyListTest\Category;
/**
* @skipUpgrade
@ -1835,4 +1835,37 @@ class DataListTest extends SapphireTest
$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);
}
}