From ff6909df9765e6bfedc1dee005c4d24f6e0310df Mon Sep 17 00:00:00 2001 From: Robert Curry Date: Fri, 20 Apr 2012 10:08:17 +1200 Subject: [PATCH] ENHANCEMENT: Add lazy loading to DataQuery. --- model/DataList.php | 11 + model/DataObject.php | 115 ++++++++- model/DataQuery.php | 129 +++++++--- tests/model/DataObjectLazyLoadingTest.php | 234 ++++++++++++++++++ tests/model/DataObjectTest.php | 13 +- tests/model/DataObjectTest.yml | 1 + tests/model/VersionedTest.php | 2 +- .../search/SearchFilterApplyRelationTest.php | 2 +- 8 files changed, 453 insertions(+), 54 deletions(-) create mode 100644 tests/model/DataObjectLazyLoadingTest.php diff --git a/model/DataList.php b/model/DataList.php index 7dcbbe234..1b854127a 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -572,6 +572,17 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab 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 diff --git a/model/DataObject.php b/model/DataObject.php index ea10a745c..94fe3715d 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -212,7 +212,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $fields = Config::inst()->get($class, 'db', Config::UNINHERITED); 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]); // Add all composite columns @@ -362,6 +362,19 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity 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; // 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) { $className = $this->class; - $clone = new $className( $this->record, false, $this->model ); + $clone = new $className( $this->toMap(), false, $this->model ); $clone->ID = 0; $clone->extend('onBeforeDuplicate', $this, $doWrite); @@ -707,6 +720,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @return array The data as a map. */ public function toMap() { + foreach ($this->record as $key => $value) { + if (strlen($key) > 5 && substr($key, -5) == '_Lazy') { + $this->loadLazyFields($value); + break; + } + } + return $this->record; } @@ -874,7 +894,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity foreach($fieldNames as $fieldName) { if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = 1; // 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 @@ -1509,7 +1529,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $classes = ClassInfo::ancestry($this); $good = false; $items = array(); - + foreach($classes as $class) { // Wait until after we reach DataObject if(!$good) { @@ -1930,7 +1950,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity public function getField($field) { // 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]; - + + // 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 if(self::is_composite_field($this->class, $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. */ 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 if($val instanceof DBField) { $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; // Situation 2: Passing a literal or non-DBField object } else { @@ -2068,7 +2154,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $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; } } @@ -2107,8 +2200,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ public function hasField($field) { return ( - array_key_exists($field, $this->record) + array_key_exists($field, $this->record) || $this->db($field) + || (substr($field,-2) == 'ID') && $this->has_one(substr($field,0, -2)) || $this->hasMethod("get{$field}") ); } @@ -2391,7 +2485,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Special case for has_one relationships } 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); // 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 = "") { Deprecation::notice('3.0', 'Use DataList::create and DataList to do your querying instead.'); return $this->extendedSQL($filter, $sort, $limit, $join, $having); - } /** diff --git a/model/DataQuery.php b/model/DataQuery.php index 1e9b03324..f56dc523a 100644 --- a/model/DataQuery.php +++ b/model/DataQuery.php @@ -24,6 +24,8 @@ class DataQuery { * @var array */ protected $collidingFields = array(); + + private $queriedColumns = null; /** * @var Boolean @@ -86,11 +88,12 @@ class DataQuery { } /** - * Set up the simplest intial query + * Set up the simplest initial query */ function initialiseQuery() { - // Get the tables to join to - $tableClasses = ClassInfo::dataClassesFor($this->dataClass); + // Get the tables to join to. + // Don't get any subclass tables - let lazy loading do that. + $tableClasses = ClassInfo::ancestry($this->dataClass, true); // Error checking if(!$tableClasses) { @@ -113,28 +116,78 @@ class DataQuery { } $this->query->from("\"$baseClass\""); - $this->selectAllFromTable($this->query, $baseClass); + $this->selectColumnsFromTable($this->query, $baseClass); singleton($this->dataClass)->extend('augmentDataQueryCreation', $this->query, $this); } + function setQueriedColumns($queriedColumns) { + $this->queriedColumns = $queriedColumns; + } + /** * Ensure that the query is ready to execute. */ - function getFinalisedQuery() { - $query = clone $this->query; - - // Get the tables to join to - $tableClasses = ClassInfo::dataClassesFor($this->dataClass); - $baseClass = array_shift($tableClasses); - - $collidingFields = array(); + function getFinalisedQuery($queriedColumns = null) { + if(!$queriedColumns) $queriedColumns = $this->queriedColumns; + if($queriedColumns) { + $queriedColumns = array_merge($queriedColumns, array('Created', 'LastEdited', 'ClassName')); + } - // Join all the tables - if($this->querySubclasses) { - foreach($tableClasses as $tableClass) { + $query = clone $this->query; + + // 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\"") ; - $this->selectAllFromTable($query, $tableClass); } } @@ -307,28 +360,28 @@ class DataQuery { /** * Update the SELECT clause of the query with the columns from the given table */ - protected function selectAllFromTable(SQLQuery &$query, $tableClass) { - // Add SQL for multi-value fields - $databaseFields = DataObject::database_fields($tableClass); - $compositeFields = DataObject::composite_fields($tableClass, false); - if($databaseFields) foreach($databaseFields as $k => $v) { - if(!isset($compositeFields[$k])) { - // Update $collidingFields if necessary - if(isset($query->select[$k])) { - if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($query->select[$k]); - $this->collidingFields[$k][] = "\"$tableClass\".\"$k\""; + protected function selectColumnsFromTable(SQLQuery &$query, $tableClass, $columns = null) { + // Add SQL for multi-value fields + $databaseFields = DataObject::database_fields($tableClass); + $compositeFields = DataObject::composite_fields($tableClass, false); + if($databaseFields) foreach($databaseFields as $k => $v) { + if((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) { + // Update $collidingFields if necessary + if(isset($query->select[$k])) { + if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($query->select[$k]); + $this->collidingFields[$k][] = "\"$tableClass\".\"$k\""; - } else { - $query->select[$k] = "\"$tableClass\".\"$k\""; - } - } - } - if($compositeFields) foreach($compositeFields as $k => $v) { - if($v) { - $dbO = Object::create_from_string($v, $k); - $dbO->addToQuery($query); - } - } + } else { + $query->select[$k] = "\"$tableClass\".\"$k\""; + } + } + } + if($compositeFields) foreach($compositeFields as $k => $v) { + if((is_null($columns) || in_array($k, $columns)) && $v) { + $dbO = Object::create_from_string($v, $k); + $dbO->addToQuery($query); + } + } } /** @@ -561,7 +614,7 @@ class DataQuery { * Query the given field column from the database and return as an array. */ public function column($field = 'ID') { - $query = $this->getFinalisedQuery(); + $query = $this->getFinalisedQuery(array($field)); $query->select($this->expressionForField($field, $query)); $this->ensureSelectContainsOrderbyColumns($query); diff --git a/tests/model/DataObjectLazyLoadingTest.php b/tests/model/DataObjectLazyLoadingTest.php new file mode 100644 index 000000000..35de74cc7 --- /dev/null +++ b/tests/model/DataObjectLazyLoadingTest.php @@ -0,0 +1,234 @@ +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()); + } +} diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index 87f6bccba..32cce1fe4 100644 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -367,7 +367,7 @@ class DataObjectTest extends SapphireTest { } 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"); foreach($itemsA as $item) $keysA[] = $item->ID; @@ -555,6 +555,7 @@ class DataObjectTest extends SapphireTest { //'Created', //'LastEdited', 'SubclassDatabaseField', + 'ParentTeamID', 'Title', 'DatabaseField', 'ExtendedDatabaseField', @@ -569,6 +570,7 @@ class DataObjectTest extends SapphireTest { array_keys(DataObject::database_fields('DataObjectTest_SubTeam')), array( 'SubclassDatabaseField', + 'ParentTeamID', ), 'databaseFields() on subclass contains only fields defined on instance' ); @@ -731,7 +733,7 @@ class DataObjectTest extends SapphireTest { function testManyManyExtraFields() { $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) $teamExtraFields = $team->many_many_extraFields('Players'); @@ -1015,7 +1017,7 @@ class DataObjectTest extends SapphireTest { $objEmpty->Title = '0'; // $this->assertFalse($objEmpty->isEmpty(), 'Zero value in attribute considered non-empty'); } - + function testRelField() { $captain = $this->objFromFixture('DataObjectTest_Player', 'captain1'); // Test traversal of a single has_one @@ -1095,6 +1097,7 @@ class DataObjectTest_Team extends DataObject implements TestOnly { ); static $has_many = array( + 'SubTeams' => 'DataObjectTest_SubTeam', 'Comments' => 'DataObjectTest_TeamComment' ); @@ -1142,6 +1145,10 @@ class DataObjectTest_SubTeam extends DataObjectTest_Team implements TestOnly { static $db = array( 'SubclassDatabaseField' => 'Varchar' ); + + static $has_one = array( + "ParentTeam" => 'DataObjectTest_Team', + ); } class OtherSubclassWithSameField extends DataObjectTest_Team implements TestOnly { static $db = array( diff --git a/tests/model/DataObjectTest.yml b/tests/model/DataObjectTest.yml index 3e40f1cde..c51741cb9 100644 --- a/tests/model/DataObjectTest.yml +++ b/tests/model/DataObjectTest.yml @@ -26,6 +26,7 @@ DataObjectTest_SubTeam: Title: Subteam 1 SubclassDatabaseField: Subclassed 1 ExtendedDatabaseField: Extended 1 + ParentTeam: =>DataObjectTest_Team.team1 subteam2_with_player_relation: Title: Subteam 2 SubclassDatabaseField: Subclassed 2 diff --git a/tests/model/VersionedTest.php b/tests/model/VersionedTest.php index d88e97ec8..99366089c 100644 --- a/tests/model/VersionedTest.php +++ b/tests/model/VersionedTest.php @@ -240,7 +240,7 @@ class VersionedTest extends SapphireTest { $this->assertEquals(array( 'VersionedTest_DataObject_Live', 'VersionedTest_Subclass_Live', - ), DataObject::get('VersionedTest_DataObject')->dataQuery()->query()->queriedTables()); + ), DataObject::get('VersionedTest_Subclass')->dataQuery()->query()->queriedTables()); } } diff --git a/tests/search/SearchFilterApplyRelationTest.php b/tests/search/SearchFilterApplyRelationTest.php index 64aa99eb1..d85dd2a33 100644 --- a/tests/search/SearchFilterApplyRelationTest.php +++ b/tests/search/SearchFilterApplyRelationTest.php @@ -153,7 +153,7 @@ class SearchFilterApplyRelationTest_HasManyParent extends DataObject implements } 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( "ChildField" => "Varchar" );