Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX isInDB actually checks if the object is in DB #7799

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/ORM/DataList.php
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,8 @@ public function createDataObject($row)

$item = Injector::inst()->create($class, $row, false, $this->getQueryParams());

DataObject::register_object($item);

return $item;
}

Expand Down
59 changes: 54 additions & 5 deletions src/ORM/DataObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use SilverStripe\ORM\Filters\SearchFilter;
use SilverStripe\ORM\Queries\SQLDelete;
use SilverStripe\ORM\Queries\SQLInsert;
use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\ORM\Search\SearchContext;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
Expand Down Expand Up @@ -225,6 +226,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
protected static $_cache_get_one;

/**
* Static caches use by isInDB to check if an ID is in the DB
*
* @var array
*/
protected static $_cache_in_db = [];

/**
* Cache of field labels
*
Expand Down Expand Up @@ -596,7 +604,7 @@ public function defineMethods()
*/
public function exists()
{
return (isset($this->record['ID']) && $this->record['ID'] > 0);
return $this->isInDB();
}

/**
Expand Down Expand Up @@ -1284,10 +1292,17 @@ protected function writeBaseRecord($baseTable, $now)
// Perform an insert on the base table
$insert = new SQLInsert('"' . $baseTable . '"');
$insert
->assign('"Created"', $now)
->execute();
->assign('"Created"', $now);
if ($id = $this->ID) {
$insert = $insert->assign('"ID"', $id);
}
$insert->execute();
if (!$id) {
$id = DB::get_generated_id($baseTable);
}
$this->changed['ID'] = self::CHANGE_VALUE;
$this->record['ID'] = DB::get_generated_id($baseTable);
$this->record['ID'] = $id;
self::register_object($this);
}

/**
Expand Down Expand Up @@ -2903,7 +2918,10 @@ public function flushCache($persistent = true)
{
if (static::class == self::class) {
self::$_cache_get_one = array();
self::$_cache_in_db = [];
return $this;
} else {
self::unregister_object($this);
}

$classes = ClassInfo::ancestry(static::class);
Expand Down Expand Up @@ -2949,6 +2967,23 @@ public static function reset()
static::getSchema()->reset();
self::$_cache_get_one = array();
self::$_cache_field_labels = array();
self::$_cache_in_db = [];
}

/**
* @param DataObject $object
*/
public static function register_object($object)
{
self::$_cache_in_db[$object->baseClass()][$object->ID] = true;
}

/**
* @param DataObject $object
*/
public function unregister_object($object)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can(should?) be marked as static too?

{
unset(self::$_cache_in_db[$object->baseClass()][$object->ID]);
}

/**
Expand Down Expand Up @@ -3413,7 +3448,21 @@ public function defaultSearchFilters()
*/
public function isInDB()
{
return is_numeric($this->ID) && $this->ID > 0;
if (empty($this->ID)) {
return false;
}
$class = $this->baseClass();
if (!isset(self::$_cache_in_db[$class][$this->ID])) {
$sqlSelect = new SQLSelect();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only way that you can get to this code is if you have manually set the ID and not yet written the record. So, this code is picking up the case when you've manually set the ID to an existing record.

We have the option of simply returning false in this case. If we did this, the functionality would match the "has been written" logic that I've recommended. It would remove the cause for performance / logic concerns that others have had.

My view would be that checking the database for the presence of an ID that has been manually applied is a narrow & different use-case from the current uses of isInDB(). In many cases where this happened it's not so much "this record is in the database" as "I am about to corrupt my database". Code responsible for ID collision would need some careful and separate thought.

So I think that should simply return false in this case, and the update the docblock of isInDB() to

Returns true if this record was read from the database or has been written to it.

Note that it does not query the database for the presence of a manually-applied ID.

Note also that, after this change, the static registry can probably be replaced with an instance-specific private boolean $isInDb variable.

$sqlSelect->setFrom('"' . $this->baseTable() . '"');
$sqlSelect->setWhere([
'"ID" = ?' => $this->ID,
]);
if ($sqlSelect->count() > 0) {
static::register_object($this);
}
}
return self::$_cache_in_db[$class][$this->ID];
}

/*
Expand Down
17 changes: 17 additions & 0 deletions tests/php/ORM/DataObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use SilverStripe\ORM\FieldType\DBVarchar;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\Tests\DataObjectTest\Player;
use SilverStripe\ORM\Tests\DataObjectTest\Staff;
use SilverStripe\View\ViewableData;
use stdClass;

Expand Down Expand Up @@ -2135,4 +2136,20 @@ public function testGetOneMissingValueReturnsNull()
['"DataObjectTest_TeamComment"."Name"' => 'does not exists']
));
}

public function testIsInDB()
{
$nonExistantID = Staff::get()->max('ID') + 1;
$staff = new Staff([
'ID' => $nonExistantID,
'Salary' => 12,
]);
$this->assertFalse($staff->isInDB());
$staff->write();
$this->assertTrue($staff->isInDB());
$staff->delete();
$this->assertFalse($staff->isInDB());
$staff->ID = $nonExistantID;
$this->assertFalse($staff->isInDB());
}
}