mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
ENHANCEMENT: Add lazy loading to DataQuery.
This commit is contained in:
parent
3ccaa1f864
commit
ff6909df97
@ -572,6 +572,17 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
|||||||
return $clone->where("$SQL_col = '" . Convert::raw2sql($value) . "'")->First();
|
return $clone->where("$SQL_col = '" . Convert::raw2sql($value) . "'")->First();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restrict the columns to fetch into this DataList
|
||||||
|
*
|
||||||
|
* @param array $queriedColumns
|
||||||
|
* @return DataList
|
||||||
|
*/
|
||||||
|
public function setQueriedColumns($queriedColumns) {
|
||||||
|
$clone = clone $this;
|
||||||
|
$clone->dataQuery->setQueriedColumns($queriedColumns);
|
||||||
|
return $clone;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter this list to only contain the given Primary IDs
|
* Filter this list to only contain the given Primary IDs
|
||||||
|
@ -212,7 +212,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
$fields = Config::inst()->get($class, 'db', Config::UNINHERITED);
|
$fields = Config::inst()->get($class, 'db', Config::UNINHERITED);
|
||||||
|
|
||||||
foreach(self::composite_fields($class, false) as $fieldName => $fieldClass) {
|
foreach(self::composite_fields($class, false) as $fieldName => $fieldClass) {
|
||||||
// Remove the original fieldname, its not an actual database column
|
// Remove the original fieldname, it's not an actual database column
|
||||||
unset($fields[$fieldName]);
|
unset($fields[$fieldName]);
|
||||||
|
|
||||||
// Add all composite columns
|
// Add all composite columns
|
||||||
@ -362,6 +362,19 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
else $this->record[$k] = $v;
|
else $this->record[$k] = $v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Identify fields that should be lazy loaded, but only on existing records
|
||||||
|
if(!empty($record['ID'])) {
|
||||||
|
$currentObj = get_class($this);
|
||||||
|
while($currentObj != 'DataObject') {
|
||||||
|
$fields = self::custom_database_fields($currentObj);
|
||||||
|
foreach($fields as $field => $type) {
|
||||||
|
if(!array_key_exists($field, $record)) $this->record[$field.'_Lazy'] = $currentObj;
|
||||||
|
}
|
||||||
|
$currentObj = get_parent_class($currentObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->original = $this->record;
|
$this->original = $this->record;
|
||||||
|
|
||||||
// Keep track of the modification date of all the data sourced to make this page
|
// Keep track of the modification date of all the data sourced to make this page
|
||||||
@ -413,7 +426,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
*/
|
*/
|
||||||
function duplicate($doWrite = true) {
|
function duplicate($doWrite = true) {
|
||||||
$className = $this->class;
|
$className = $this->class;
|
||||||
$clone = new $className( $this->record, false, $this->model );
|
$clone = new $className( $this->toMap(), false, $this->model );
|
||||||
$clone->ID = 0;
|
$clone->ID = 0;
|
||||||
|
|
||||||
$clone->extend('onBeforeDuplicate', $this, $doWrite);
|
$clone->extend('onBeforeDuplicate', $this, $doWrite);
|
||||||
@ -707,6 +720,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
* @return array The data as a map.
|
* @return array The data as a map.
|
||||||
*/
|
*/
|
||||||
public function toMap() {
|
public function toMap() {
|
||||||
|
foreach ($this->record as $key => $value) {
|
||||||
|
if (strlen($key) > 5 && substr($key, -5) == '_Lazy') {
|
||||||
|
$this->loadLazyFields($value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $this->record;
|
return $this->record;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -874,7 +894,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
foreach($fieldNames as $fieldName) {
|
foreach($fieldNames as $fieldName) {
|
||||||
if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = 1;
|
if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = 1;
|
||||||
// Populate the null values in record so that they actually get written
|
// Populate the null values in record so that they actually get written
|
||||||
if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
|
if(!$this->$fieldName) $this->record[$fieldName] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @todo Find better way to allow versioned to write a new version after forceChange
|
// @todo Find better way to allow versioned to write a new version after forceChange
|
||||||
@ -1509,7 +1529,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
$classes = ClassInfo::ancestry($this);
|
$classes = ClassInfo::ancestry($this);
|
||||||
$good = false;
|
$good = false;
|
||||||
$items = array();
|
$items = array();
|
||||||
|
|
||||||
foreach($classes as $class) {
|
foreach($classes as $class) {
|
||||||
// Wait until after we reach DataObject
|
// Wait until after we reach DataObject
|
||||||
if(!$good) {
|
if(!$good) {
|
||||||
@ -1930,7 +1950,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
public function getField($field) {
|
public function getField($field) {
|
||||||
// If we already have an object in $this->record, then we should just return that
|
// If we already have an object in $this->record, then we should just return that
|
||||||
if(isset($this->record[$field]) && is_object($this->record[$field])) return $this->record[$field];
|
if(isset($this->record[$field]) && is_object($this->record[$field])) return $this->record[$field];
|
||||||
|
|
||||||
|
// Do we have a field that needs to be lazy loaded?
|
||||||
|
if(isset($this->record[$field.'_Lazy'])) {
|
||||||
|
$tableClass = $this->record[$field.'_Lazy'];
|
||||||
|
$this->loadLazyFields($tableClass);
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise, we need to determine if this is a complex field
|
// Otherwise, we need to determine if this is a complex field
|
||||||
if(self::is_composite_field($this->class, $field)) {
|
if(self::is_composite_field($this->class, $field)) {
|
||||||
$helper = $this->castingHelper($field);
|
$helper = $this->castingHelper($field);
|
||||||
@ -1950,12 +1976,64 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a map of all the fields for this record.
|
* Return a map of all the fields for this record
|
||||||
|
* @deprecated 2.4 Use toMap()
|
||||||
*
|
*
|
||||||
* @return array A map of field names to field values.
|
* @return array A map of field names to field values.
|
||||||
*/
|
*/
|
||||||
public function getAllFields() {
|
public function getAllFields() {
|
||||||
return $this->record;
|
return $this->toMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all the stub fields than an initial lazy load didn't load fully.
|
||||||
|
*
|
||||||
|
* @param tableClass Base table to load the values from. Others are joined as required.
|
||||||
|
*/
|
||||||
|
|
||||||
|
protected function loadLazyFields($tableClass = null) {
|
||||||
|
// Smarter way to work out the tableClass? Should the functionality in toMap and getField be moved into here?
|
||||||
|
if (!$tableClass) $tableClass = $this->ClassName;
|
||||||
|
|
||||||
|
$dataQuery = new DataQuery($tableClass);
|
||||||
|
$dataQuery->where("\"$tableClass\".\"ID\" = {$this->record['ID']}")->limit(1);
|
||||||
|
$columns = array();
|
||||||
|
|
||||||
|
// Add SQL for fields, both simple & multi-value
|
||||||
|
// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
|
||||||
|
$databaseFields = self::database_fields($tableClass);
|
||||||
|
if($databaseFields) foreach($databaseFields as $k => $v) {
|
||||||
|
if(!isset($this->record[$k]) || $this->record[$k] === null) {
|
||||||
|
$columns[] = $k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($columns) {
|
||||||
|
$query = $dataQuery->query(); // eh?
|
||||||
|
$this->extend('augmentSQL', $query, $dataQuery);
|
||||||
|
|
||||||
|
$dataQuery->setQueriedColumns($columns);
|
||||||
|
$newData = $dataQuery->execute()->record();
|
||||||
|
|
||||||
|
// Load the data into record
|
||||||
|
if($newData) {
|
||||||
|
foreach($newData as $k => $v) {
|
||||||
|
if (in_array($k, $columns)) {
|
||||||
|
$this->record[$k] = $v;
|
||||||
|
$this->original[$k] = $v;
|
||||||
|
unset($this->record[$k . '_Lazy']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No data means that the query returned nothing; assign 'null' to all the requested fields
|
||||||
|
} else {
|
||||||
|
foreach($columns as $k) {
|
||||||
|
$this->record[$k] = null;
|
||||||
|
$this->original[$k] = null;
|
||||||
|
unset($this->record[$k . '_Lazy']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2047,6 +2125,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
// Situation 1: Passing an DBField
|
// Situation 1: Passing an DBField
|
||||||
if($val instanceof DBField) {
|
if($val instanceof DBField) {
|
||||||
$val->Name = $fieldName;
|
$val->Name = $fieldName;
|
||||||
|
|
||||||
|
// If we've just lazy-loaded the column, then we need to populate the $original array by
|
||||||
|
// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
|
||||||
|
// on a call to getChanged()?
|
||||||
|
if (isset($this->record[$fieldName.'_Lazy'])) {
|
||||||
|
$this->getField($fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
$this->record[$fieldName] = $val;
|
$this->record[$fieldName] = $val;
|
||||||
// Situation 2: Passing a literal or non-DBField object
|
// Situation 2: Passing a literal or non-DBField object
|
||||||
} else {
|
} else {
|
||||||
@ -2068,7 +2154,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
$this->changed[$fieldName] = 2;
|
$this->changed[$fieldName] = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// value is always saved back when strict check succeeds
|
// If we've just lazy-loaded the column, then we need to populate the $original array by
|
||||||
|
// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
|
||||||
|
// on a call to getChanged()?
|
||||||
|
if (isset($this->record[$fieldName.'_Lazy'])) {
|
||||||
|
$this->getField($fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value is always saved back when strict check succeeds.
|
||||||
$this->record[$fieldName] = $val;
|
$this->record[$fieldName] = $val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2107,8 +2200,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
*/
|
*/
|
||||||
public function hasField($field) {
|
public function hasField($field) {
|
||||||
return (
|
return (
|
||||||
array_key_exists($field, $this->record)
|
array_key_exists($field, $this->record)
|
||||||
|| $this->db($field)
|
|| $this->db($field)
|
||||||
|
|| (substr($field,-2) == 'ID') && $this->has_one(substr($field,0, -2))
|
||||||
|| $this->hasMethod("get{$field}")
|
|| $this->hasMethod("get{$field}")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2391,7 +2485,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
|
|
||||||
// Special case for has_one relationships
|
// Special case for has_one relationships
|
||||||
} else if(preg_match('/ID$/', $fieldName) && $this->has_one(substr($fieldName,0,-2))) {
|
} else if(preg_match('/ID$/', $fieldName) && $this->has_one(substr($fieldName,0,-2))) {
|
||||||
$val = (isset($this->record[$fieldName])) ? $this->record[$fieldName] : null;
|
$val = $this->$fieldName;
|
||||||
return DBField::create_field('ForeignKey', $val, $fieldName, $this);
|
return DBField::create_field('ForeignKey', $val, $fieldName, $this);
|
||||||
|
|
||||||
// Special case for ClassName
|
// Special case for ClassName
|
||||||
@ -2496,7 +2590,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
public function buildSQL($filter = "", $sort = "", $limit = "", $join = "", $restrictClasses = true, $having = "") {
|
public function buildSQL($filter = "", $sort = "", $limit = "", $join = "", $restrictClasses = true, $having = "") {
|
||||||
Deprecation::notice('3.0', 'Use DataList::create and DataList to do your querying instead.');
|
Deprecation::notice('3.0', 'Use DataList::create and DataList to do your querying instead.');
|
||||||
return $this->extendedSQL($filter, $sort, $limit, $join, $having);
|
return $this->extendedSQL($filter, $sort, $limit, $join, $having);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,6 +24,8 @@ class DataQuery {
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $collidingFields = array();
|
protected $collidingFields = array();
|
||||||
|
|
||||||
|
private $queriedColumns = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Boolean
|
* @var Boolean
|
||||||
@ -86,11 +88,12 @@ class DataQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up the simplest intial query
|
* Set up the simplest initial query
|
||||||
*/
|
*/
|
||||||
function initialiseQuery() {
|
function initialiseQuery() {
|
||||||
// Get the tables to join to
|
// Get the tables to join to.
|
||||||
$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
|
// Don't get any subclass tables - let lazy loading do that.
|
||||||
|
$tableClasses = ClassInfo::ancestry($this->dataClass, true);
|
||||||
|
|
||||||
// Error checking
|
// Error checking
|
||||||
if(!$tableClasses) {
|
if(!$tableClasses) {
|
||||||
@ -113,28 +116,78 @@ class DataQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->query->from("\"$baseClass\"");
|
$this->query->from("\"$baseClass\"");
|
||||||
$this->selectAllFromTable($this->query, $baseClass);
|
$this->selectColumnsFromTable($this->query, $baseClass);
|
||||||
|
|
||||||
singleton($this->dataClass)->extend('augmentDataQueryCreation', $this->query, $this);
|
singleton($this->dataClass)->extend('augmentDataQueryCreation', $this->query, $this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setQueriedColumns($queriedColumns) {
|
||||||
|
$this->queriedColumns = $queriedColumns;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that the query is ready to execute.
|
* Ensure that the query is ready to execute.
|
||||||
*/
|
*/
|
||||||
function getFinalisedQuery() {
|
function getFinalisedQuery($queriedColumns = null) {
|
||||||
$query = clone $this->query;
|
if(!$queriedColumns) $queriedColumns = $this->queriedColumns;
|
||||||
|
if($queriedColumns) {
|
||||||
// Get the tables to join to
|
$queriedColumns = array_merge($queriedColumns, array('Created', 'LastEdited', 'ClassName'));
|
||||||
$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
|
}
|
||||||
$baseClass = array_shift($tableClasses);
|
|
||||||
|
|
||||||
$collidingFields = array();
|
|
||||||
|
|
||||||
// Join all the tables
|
$query = clone $this->query;
|
||||||
if($this->querySubclasses) {
|
|
||||||
foreach($tableClasses as $tableClass) {
|
// Generate the list of tables to iterate over and the list of columns required by any existing where clauses.
|
||||||
|
// This second step is skipped if we're fetching the whole dataobject as any required columns will get selected
|
||||||
|
// regardless.
|
||||||
|
if($queriedColumns) {
|
||||||
|
$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
|
||||||
|
|
||||||
|
foreach ($query->where as $where) {
|
||||||
|
// Check for just the column, in the form '"Column" = ?' and the form '"Table"."Column"' = ?
|
||||||
|
if (preg_match('/^"([^"]+)"/', $where, $matches) ||
|
||||||
|
preg_match('/^"([^"]+)"\."[^"]+"/', $where, $matches)) {
|
||||||
|
if (!in_array($matches[1], $queriedColumns)) $queriedColumns[] = $matches[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else $tableClasses = ClassInfo::ancestry($this->dataClass, true);
|
||||||
|
|
||||||
|
$tableNames = array_keys($tableClasses);
|
||||||
|
$baseClass = $tableNames[0];
|
||||||
|
|
||||||
|
// Empty the existing select query of all non-generated selects (eg, random sorts and many-many-extrafields).
|
||||||
|
// Maybe we should remove all fields that exist on this class instead?
|
||||||
|
foreach ($query->select as $name => $column) {
|
||||||
|
if (!is_numeric($name)) unset($query->select[$name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over the tables and check what we need to select from them. If any selects are made (or the table is
|
||||||
|
// required for a select)
|
||||||
|
foreach($tableClasses as $tableClass) {
|
||||||
|
$joinTable = false;
|
||||||
|
|
||||||
|
// If queriedColumns is set, then check if any of the fields are in this table.
|
||||||
|
if ($queriedColumns) {
|
||||||
|
$tableFields = DataObject::database_fields($tableClass);
|
||||||
|
$selectColumns = array();
|
||||||
|
// Look through columns specifically requested in query (or where clause)
|
||||||
|
foreach ($queriedColumns as $queriedColumn) {
|
||||||
|
if (array_key_exists($queriedColumn, $tableFields)) {
|
||||||
|
$selectColumns[] = $queriedColumn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->selectColumnsFromTable($query, $tableClass, $selectColumns);
|
||||||
|
if ($selectColumns && $tableClass != $baseClass) {
|
||||||
|
$joinTable = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->selectColumnsFromTable($query, $tableClass);
|
||||||
|
if ($tableClass != $baseClass) $joinTable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($joinTable) {
|
||||||
$query->leftJoin($tableClass, "\"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"") ;
|
$query->leftJoin($tableClass, "\"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"") ;
|
||||||
$this->selectAllFromTable($query, $tableClass);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,28 +360,28 @@ class DataQuery {
|
|||||||
/**
|
/**
|
||||||
* Update the SELECT clause of the query with the columns from the given table
|
* Update the SELECT clause of the query with the columns from the given table
|
||||||
*/
|
*/
|
||||||
protected function selectAllFromTable(SQLQuery &$query, $tableClass) {
|
protected function selectColumnsFromTable(SQLQuery &$query, $tableClass, $columns = null) {
|
||||||
// Add SQL for multi-value fields
|
// Add SQL for multi-value fields
|
||||||
$databaseFields = DataObject::database_fields($tableClass);
|
$databaseFields = DataObject::database_fields($tableClass);
|
||||||
$compositeFields = DataObject::composite_fields($tableClass, false);
|
$compositeFields = DataObject::composite_fields($tableClass, false);
|
||||||
if($databaseFields) foreach($databaseFields as $k => $v) {
|
if($databaseFields) foreach($databaseFields as $k => $v) {
|
||||||
if(!isset($compositeFields[$k])) {
|
if((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) {
|
||||||
// Update $collidingFields if necessary
|
// Update $collidingFields if necessary
|
||||||
if(isset($query->select[$k])) {
|
if(isset($query->select[$k])) {
|
||||||
if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($query->select[$k]);
|
if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($query->select[$k]);
|
||||||
$this->collidingFields[$k][] = "\"$tableClass\".\"$k\"";
|
$this->collidingFields[$k][] = "\"$tableClass\".\"$k\"";
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$query->select[$k] = "\"$tableClass\".\"$k\"";
|
$query->select[$k] = "\"$tableClass\".\"$k\"";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if($compositeFields) foreach($compositeFields as $k => $v) {
|
if($compositeFields) foreach($compositeFields as $k => $v) {
|
||||||
if($v) {
|
if((is_null($columns) || in_array($k, $columns)) && $v) {
|
||||||
$dbO = Object::create_from_string($v, $k);
|
$dbO = Object::create_from_string($v, $k);
|
||||||
$dbO->addToQuery($query);
|
$dbO->addToQuery($query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -561,7 +614,7 @@ 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.
|
||||||
*/
|
*/
|
||||||
public function column($field = 'ID') {
|
public function column($field = 'ID') {
|
||||||
$query = $this->getFinalisedQuery();
|
$query = $this->getFinalisedQuery(array($field));
|
||||||
$query->select($this->expressionForField($field, $query));
|
$query->select($this->expressionForField($field, $query));
|
||||||
$this->ensureSelectContainsOrderbyColumns($query);
|
$this->ensureSelectContainsOrderbyColumns($query);
|
||||||
|
|
||||||
|
234
tests/model/DataObjectLazyLoadingTest.php
Normal file
234
tests/model/DataObjectLazyLoadingTest.php
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package framework
|
||||||
|
* @subpackage tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DataObjectLazyLoadingTest extends SapphireTest {
|
||||||
|
|
||||||
|
static $fixture_file = 'DataObjectTest.yml';
|
||||||
|
|
||||||
|
// These are all defined in DataObjectTest.php
|
||||||
|
protected $extraDataObjects = array(
|
||||||
|
'DataObjectTest_Team',
|
||||||
|
'DataObjectTest_Fixture',
|
||||||
|
'DataObjectTest_SubTeam',
|
||||||
|
'OtherSubclassWithSameField',
|
||||||
|
'DataObjectTest_FieldlessTable',
|
||||||
|
'DataObjectTest_FieldlessSubTable',
|
||||||
|
'DataObjectTest_ValidatedObject',
|
||||||
|
'DataObjectTest_Player',
|
||||||
|
'DataObjectTest_TeamComment'
|
||||||
|
);
|
||||||
|
|
||||||
|
function testQueriedColumnsID() {
|
||||||
|
$playerList = new DataList('DataObjectTest_SubTeam');
|
||||||
|
$playerList = $playerList->setQueriedColumns(array('ID'));
|
||||||
|
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."Created", ' .
|
||||||
|
'"DataObjectTest_Team"."LastEdited", "DataObjectTest_Team"."ID", CASE WHEN '.
|
||||||
|
'"DataObjectTest_Team"."ClassName" IS NOT NULL THEN "DataObjectTest_Team"."ClassName" ELSE ' .
|
||||||
|
'\'DataObjectTest_Team\' END AS "RecordClassName" FROM "DataObjectTest_Team" WHERE ' .
|
||||||
|
'("DataObjectTest_Team"."ClassName" IN (\'DataObjectTest_SubTeam\'))';
|
||||||
|
$this->assertEquals($expected, $playerList->sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testQueriedColumnsFromBaseTableAndSubTable() {
|
||||||
|
$playerList = new DataList('DataObjectTest_SubTeam');
|
||||||
|
$playerList = $playerList->setQueriedColumns(array('Title', 'SubclassDatabaseField'));
|
||||||
|
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."Created", ' .
|
||||||
|
'"DataObjectTest_Team"."LastEdited", "DataObjectTest_Team"."Title", ' .
|
||||||
|
'"DataObjectTest_SubTeam"."SubclassDatabaseField", "DataObjectTest_Team"."ID", CASE WHEN ' .
|
||||||
|
'"DataObjectTest_Team"."ClassName" IS NOT NULL THEN "DataObjectTest_Team"."ClassName" ELSE ' .
|
||||||
|
'\'DataObjectTest_Team\' END AS "RecordClassName" FROM "DataObjectTest_Team" LEFT JOIN ' .
|
||||||
|
'"DataObjectTest_SubTeam" ON "DataObjectTest_SubTeam"."ID" = "DataObjectTest_Team"."ID" WHERE ' .
|
||||||
|
'("DataObjectTest_Team"."ClassName" IN (\'DataObjectTest_SubTeam\'))';
|
||||||
|
$this->assertEquals($expected, $playerList->sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testQueriedColumnsFromBaseTable() {
|
||||||
|
$playerList = new DataList('DataObjectTest_SubTeam');
|
||||||
|
$playerList = $playerList->setQueriedColumns(array('Title'));
|
||||||
|
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."Created", ' .
|
||||||
|
'"DataObjectTest_Team"."LastEdited", "DataObjectTest_Team"."Title", "DataObjectTest_Team"."ID", ' .
|
||||||
|
'CASE WHEN "DataObjectTest_Team"."ClassName" IS NOT NULL THEN "DataObjectTest_Team"."ClassName" ELSE ' .
|
||||||
|
'\'DataObjectTest_Team\' END AS "RecordClassName" FROM "DataObjectTest_Team" WHERE ' .
|
||||||
|
'("DataObjectTest_Team"."ClassName" IN (\'DataObjectTest_SubTeam\'))';
|
||||||
|
$this->assertEquals($expected, $playerList->sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testQueriedColumnsFromSubTable() {
|
||||||
|
$playerList = new DataList('DataObjectTest_SubTeam');
|
||||||
|
$playerList = $playerList->setQueriedColumns(array('SubclassDatabaseField'));
|
||||||
|
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."Created", ' .
|
||||||
|
'"DataObjectTest_Team"."LastEdited", "DataObjectTest_SubTeam"."SubclassDatabaseField", ' .
|
||||||
|
'"DataObjectTest_Team"."ID", CASE WHEN "DataObjectTest_Team"."ClassName" IS NOT NULL THEN ' .
|
||||||
|
'"DataObjectTest_Team"."ClassName" ELSE \'DataObjectTest_Team\' END AS "RecordClassName" FROM ' .
|
||||||
|
'"DataObjectTest_Team" LEFT JOIN "DataObjectTest_SubTeam" ON "DataObjectTest_SubTeam"."ID" = ' .
|
||||||
|
'"DataObjectTest_Team"."ID" WHERE ("DataObjectTest_Team"."ClassName" IN (\'DataObjectTest_SubTeam\'))';
|
||||||
|
$this->assertEquals($expected, $playerList->sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testNoSpecificColumnNamesBaseDataObjectQuery() {
|
||||||
|
// This queries all columns from base table
|
||||||
|
$playerList = new DataList('DataObjectTest_Team');
|
||||||
|
// Shouldn't be a left join in here.
|
||||||
|
$this->assertEquals(0, preg_match('/SELECT DISTINCT "DataObjectTest_Team"."ID" .* LEFT JOIN .* FROM "DataObjectTest_Team"/', $playerList->sql()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testNoSpecificColumnNamesSubclassDataObjectQuery() {
|
||||||
|
// This queries all columns from base table and subtable
|
||||||
|
$playerList = new DataList('DataObjectTest_SubTeam');
|
||||||
|
// Should be a left join.
|
||||||
|
$this->assertEquals(1, preg_match('/SELECT DISTINCT .* LEFT JOIN .* /', $playerList->sql()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLazyLoadedFieldsHasField() {
|
||||||
|
$subteam1 = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
|
||||||
|
$teams = DataObject::get('DataObjectTest_Team'); // query parent class
|
||||||
|
$subteam1Lazy = $teams->find('ID', $subteam1->ID);
|
||||||
|
|
||||||
|
// TODO Fix hasField() to exclude *_Lazy
|
||||||
|
// $this->assertFalse($subteam1Lazy->hasField('SubclassDatabaseField_Lazy'));
|
||||||
|
$this->assertTrue($subteam1Lazy->hasField('SubclassDatabaseField'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLazyLoadedFieldsGetField() {
|
||||||
|
$subteam1 = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
|
||||||
|
$teams = DataObject::get('DataObjectTest_Team'); // query parent class
|
||||||
|
$subteam1Lazy = $teams->find('ID', $subteam1->ID);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
$subteam1->getField('SubclassDatabaseField'),
|
||||||
|
$subteam1Lazy->getField('SubclassDatabaseField')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLazyLoadedFieldsSetField() {
|
||||||
|
$subteam1 = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
|
||||||
|
$subteam1ID = $subteam1->ID;
|
||||||
|
$teams = DataObject::get('DataObjectTest_Team'); // query parent class
|
||||||
|
$subteam1Lazy = $teams->find('ID', $subteam1->ID);
|
||||||
|
|
||||||
|
// Updated lazyloaded field
|
||||||
|
$subteam1Lazy->SubclassDatabaseField = 'Changed';
|
||||||
|
$subteam1Lazy->write();
|
||||||
|
|
||||||
|
// Reload from database
|
||||||
|
DataObject::flush_and_destroy_cache();
|
||||||
|
$subteam1Reloaded = DataObject::get_by_id('DataObjectTest_SubTeam', $subteam1ID);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'Changed',
|
||||||
|
$subteam1Reloaded->getField('SubclassDatabaseField')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLazyLoadedFieldsWriteWithUnloadedFields() {
|
||||||
|
$subteam1 = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
|
||||||
|
$subteam1ID = $subteam1->ID;
|
||||||
|
$teams = DataObject::get('DataObjectTest_Team'); // query parent class
|
||||||
|
$subteam1Lazy = $teams->find('ID', $subteam1->ID);
|
||||||
|
|
||||||
|
// Updated lazyloaded field
|
||||||
|
$subteam1Lazy->Title = 'Changed';
|
||||||
|
$subteam1Lazy->write();
|
||||||
|
|
||||||
|
// Reload from database
|
||||||
|
DataObject::flush_and_destroy_cache();
|
||||||
|
$subteam1Reloaded = DataObject::get_by_id('DataObjectTest_SubTeam', $subteam1ID);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'Subclassed 1',
|
||||||
|
$subteam1Reloaded->getField('SubclassDatabaseField')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLazyLoadedFieldsWriteNullFields() {
|
||||||
|
$subteam1 = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
|
||||||
|
$subteam1ID = $subteam1->ID;
|
||||||
|
$teams = DataObject::get('DataObjectTest_Team'); // query parent class
|
||||||
|
$subteam1Lazy = $teams->find('ID', $subteam1->ID);
|
||||||
|
|
||||||
|
// Updated lazyloaded field
|
||||||
|
$subteam1Lazy->SubclassDatabaseField = null;
|
||||||
|
$subteam1Lazy->write();
|
||||||
|
|
||||||
|
// Reload from database
|
||||||
|
DataObject::flush_and_destroy_cache();
|
||||||
|
$subteam1Reloaded = DataObject::get_by_id('DataObjectTest_SubTeam', $subteam1ID);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
null,
|
||||||
|
$subteam1Reloaded->getField('SubclassDatabaseField')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLazyLoadedFieldsGetChangedFields() {
|
||||||
|
$subteam1 = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
|
||||||
|
$teams = DataObject::get('DataObjectTest_Team'); // query parent class
|
||||||
|
$subteam1Lazy = $teams->find('ID', $subteam1->ID);
|
||||||
|
|
||||||
|
// Updated lazyloaded field
|
||||||
|
$subteam1Lazy->SubclassDatabaseField = 'Changed';
|
||||||
|
$this->assertEquals(
|
||||||
|
array('SubclassDatabaseField' => array(
|
||||||
|
'before' => 'Subclassed 1',
|
||||||
|
'after' => 'Changed',
|
||||||
|
'level' => 2
|
||||||
|
)),
|
||||||
|
$subteam1Lazy->getChangedFields()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLazyLoadedFieldsHasOneRelation() {
|
||||||
|
$subteam1 = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
|
||||||
|
$parentTeam = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||||
|
$teams = DataObject::get('DataObjectTest_Team'); // query parent class
|
||||||
|
$subteam1Lazy = $teams->find('ID', $subteam1->ID);
|
||||||
|
|
||||||
|
$parentTeamLazy = $subteam1Lazy->ParentTeam();
|
||||||
|
$this->assertType('DataObjectTest_Team', $parentTeamLazy);
|
||||||
|
$this->assertEquals($parentTeam->ID, $parentTeamLazy->ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLazyLoadedFieldsToMap() {
|
||||||
|
$subteam1 = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
|
||||||
|
$parentTeam = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||||
|
$teams = DataObject::get('DataObjectTest_Team'); // query parent class
|
||||||
|
$subteam1Lazy = $teams->find('ID', $subteam1->ID);
|
||||||
|
$mapLazy = $subteam1Lazy->toMap();
|
||||||
|
$this->assertArrayHasKey('SubclassDatabaseField', $mapLazy);
|
||||||
|
$this->assertEquals('Subclassed 1', $mapLazy['SubclassDatabaseField']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLazyLoadedFieldsIsEmpty() {
|
||||||
|
$subteam1 = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
|
||||||
|
$parentTeam = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||||
|
$teams = DataObject::get('DataObjectTest_Team'); // query parent class
|
||||||
|
$subteam1Lazy = $teams->find('ID', $subteam1->ID);
|
||||||
|
$subteam1Lazy->Title = '';
|
||||||
|
$subteam1Lazy->DecoratedDatabaseField = '';
|
||||||
|
$subteam1Lazy->ParentTeamID = 0;
|
||||||
|
// Leave $subteam1Lazy->SubclassDatabaseField intact
|
||||||
|
$this->assertFalse($subteam1Lazy->isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLazyLoadedFieldsDuplicate() {
|
||||||
|
$subteam1 = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
|
||||||
|
$parentTeam = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||||
|
$teams = DataObject::get('DataObjectTest_Team'); // query parent class
|
||||||
|
$subteam1Lazy = $teams->find('ID', $subteam1->ID);
|
||||||
|
$subteam1LazyDup = $subteam1Lazy->duplicate();
|
||||||
|
|
||||||
|
$this->assertEquals('Subclassed 1', $subteam1LazyDup->SubclassDatabaseField);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLazyLoadedFieldsGetAllFields() {
|
||||||
|
$subteam1 = $this->objFromFixture('DataObjectTest_SubTeam', 'subteam1');
|
||||||
|
$parentTeam = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||||
|
$teams = DataObject::get('DataObjectTest_Team'); // query parent class
|
||||||
|
$subteam1Lazy = $teams->find('ID', $subteam1->ID);
|
||||||
|
$this->assertArrayNotHasKey('SubclassDatabaseField_Lazy', $subteam1Lazy->getAllFields());
|
||||||
|
$this->assertArrayHasKey('SubclassDatabaseField', $subteam1Lazy->getAllFields());
|
||||||
|
}
|
||||||
|
}
|
@ -367,7 +367,7 @@ class DataObjectTest extends SapphireTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function testRandomSort() {
|
function testRandomSort() {
|
||||||
/* If we perforn the same regularly sorted query twice, it should return the same results */
|
/* If we perform the same regularly sorted query twice, it should return the same results */
|
||||||
$itemsA = DataObject::get("DataObjectTest_TeamComment", "", "ID");
|
$itemsA = DataObject::get("DataObjectTest_TeamComment", "", "ID");
|
||||||
foreach($itemsA as $item) $keysA[] = $item->ID;
|
foreach($itemsA as $item) $keysA[] = $item->ID;
|
||||||
|
|
||||||
@ -555,6 +555,7 @@ class DataObjectTest extends SapphireTest {
|
|||||||
//'Created',
|
//'Created',
|
||||||
//'LastEdited',
|
//'LastEdited',
|
||||||
'SubclassDatabaseField',
|
'SubclassDatabaseField',
|
||||||
|
'ParentTeamID',
|
||||||
'Title',
|
'Title',
|
||||||
'DatabaseField',
|
'DatabaseField',
|
||||||
'ExtendedDatabaseField',
|
'ExtendedDatabaseField',
|
||||||
@ -569,6 +570,7 @@ class DataObjectTest extends SapphireTest {
|
|||||||
array_keys(DataObject::database_fields('DataObjectTest_SubTeam')),
|
array_keys(DataObject::database_fields('DataObjectTest_SubTeam')),
|
||||||
array(
|
array(
|
||||||
'SubclassDatabaseField',
|
'SubclassDatabaseField',
|
||||||
|
'ParentTeamID',
|
||||||
),
|
),
|
||||||
'databaseFields() on subclass contains only fields defined on instance'
|
'databaseFields() on subclass contains only fields defined on instance'
|
||||||
);
|
);
|
||||||
@ -731,7 +733,7 @@ class DataObjectTest extends SapphireTest {
|
|||||||
|
|
||||||
function testManyManyExtraFields() {
|
function testManyManyExtraFields() {
|
||||||
$player = $this->objFromFixture('DataObjectTest_Player', 'player1');
|
$player = $this->objFromFixture('DataObjectTest_Player', 'player1');
|
||||||
$team = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
$team = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||||
|
|
||||||
// Extra fields are immediately available on the Team class (defined in $many_many_extraFields)
|
// Extra fields are immediately available on the Team class (defined in $many_many_extraFields)
|
||||||
$teamExtraFields = $team->many_many_extraFields('Players');
|
$teamExtraFields = $team->many_many_extraFields('Players');
|
||||||
@ -1015,7 +1017,7 @@ class DataObjectTest extends SapphireTest {
|
|||||||
$objEmpty->Title = '0'; //
|
$objEmpty->Title = '0'; //
|
||||||
$this->assertFalse($objEmpty->isEmpty(), 'Zero value in attribute considered non-empty');
|
$this->assertFalse($objEmpty->isEmpty(), 'Zero value in attribute considered non-empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
function testRelField() {
|
function testRelField() {
|
||||||
$captain = $this->objFromFixture('DataObjectTest_Player', 'captain1');
|
$captain = $this->objFromFixture('DataObjectTest_Player', 'captain1');
|
||||||
// Test traversal of a single has_one
|
// Test traversal of a single has_one
|
||||||
@ -1095,6 +1097,7 @@ class DataObjectTest_Team extends DataObject implements TestOnly {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static $has_many = array(
|
static $has_many = array(
|
||||||
|
'SubTeams' => 'DataObjectTest_SubTeam',
|
||||||
'Comments' => 'DataObjectTest_TeamComment'
|
'Comments' => 'DataObjectTest_TeamComment'
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1142,6 +1145,10 @@ class DataObjectTest_SubTeam extends DataObjectTest_Team implements TestOnly {
|
|||||||
static $db = array(
|
static $db = array(
|
||||||
'SubclassDatabaseField' => 'Varchar'
|
'SubclassDatabaseField' => 'Varchar'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static $has_one = array(
|
||||||
|
"ParentTeam" => 'DataObjectTest_Team',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
class OtherSubclassWithSameField extends DataObjectTest_Team implements TestOnly {
|
class OtherSubclassWithSameField extends DataObjectTest_Team implements TestOnly {
|
||||||
static $db = array(
|
static $db = array(
|
||||||
|
@ -26,6 +26,7 @@ DataObjectTest_SubTeam:
|
|||||||
Title: Subteam 1
|
Title: Subteam 1
|
||||||
SubclassDatabaseField: Subclassed 1
|
SubclassDatabaseField: Subclassed 1
|
||||||
ExtendedDatabaseField: Extended 1
|
ExtendedDatabaseField: Extended 1
|
||||||
|
ParentTeam: =>DataObjectTest_Team.team1
|
||||||
subteam2_with_player_relation:
|
subteam2_with_player_relation:
|
||||||
Title: Subteam 2
|
Title: Subteam 2
|
||||||
SubclassDatabaseField: Subclassed 2
|
SubclassDatabaseField: Subclassed 2
|
||||||
|
@ -240,7 +240,7 @@ class VersionedTest extends SapphireTest {
|
|||||||
$this->assertEquals(array(
|
$this->assertEquals(array(
|
||||||
'VersionedTest_DataObject_Live',
|
'VersionedTest_DataObject_Live',
|
||||||
'VersionedTest_Subclass_Live',
|
'VersionedTest_Subclass_Live',
|
||||||
), DataObject::get('VersionedTest_DataObject')->dataQuery()->query()->queriedTables());
|
), DataObject::get('VersionedTest_Subclass')->dataQuery()->query()->queriedTables());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ class SearchFilterApplyRelationTest_HasManyParent extends DataObject implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SearchFilterApplyRelationTest_HasManyChild extends SearchFilterApplyRelationTest_HasManyParent implements TestOnly {
|
class SearchFilterApplyRelationTest_HasManyChild extends SearchFilterApplyRelationTest_HasManyParent implements TestOnly {
|
||||||
// This is to create an seperate Table only.
|
// This is to create an separate Table only.
|
||||||
static $db = array(
|
static $db = array(
|
||||||
"ChildField" => "Varchar"
|
"ChildField" => "Varchar"
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user