ENHANCEMENT: Add lazy loading to DataQuery.

This commit is contained in:
Robert Curry 2012-04-20 10:08:17 +12:00
parent 3ccaa1f864
commit ff6909df97
8 changed files with 453 additions and 54 deletions

View File

@ -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

View File

@ -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);
} }
/** /**

View File

@ -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);

View 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());
}
}

View File

@ -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(

View File

@ -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

View File

@ -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());
} }
} }

View File

@ -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"
); );