diff --git a/application/Espo/ORM/DB/Mapper.php b/application/Espo/ORM/DB/Mapper.php index 16f1aece6e..7632e7fced 100644 --- a/application/Espo/ORM/DB/Mapper.php +++ b/application/Espo/ORM/DB/Mapper.php @@ -718,7 +718,7 @@ abstract class Mapper implements IMapper public function insert(IEntity $entity) { - $dataArr = $this->toArray($entity); + $dataArr = $this->toValueMap($entity); $fieldArr = array(); $valArr = array(); @@ -745,33 +745,35 @@ abstract class Mapper implements IMapper public function update(IEntity $entity) { - $dataArr = $this->toArray($entity); + $valueMap = $this->toValueMap($entity); - $setArr = array(); - foreach ($dataArr as $field => $value) { - if ($field == 'id') { + $setArr = []; + + foreach ($valueMap as $attribute => $value) { + if ($attribute == 'id') { continue; } - $type = $entity->fields[$field]['type']; + $type = $entity->getAttributeType($attribute); if ($type == IEntity::FOREIGN) { continue; } - if ($entity->getFetched($field) === $value && $type != IEntity::JSON_ARRAY && $type != IEntity::JSON_OBJECT) { + if (!$entity->isAttributeChanged($attribute)) { continue; } $value = $this->prepareValueForInsert($type, $value); - $setArr[] = "`" . $this->toDb($field) . "` = " . $this->quote($value); + $setArr[] = "`" . $this->toDb($attribute) . "` = " . $this->quote($value); } + if (count($setArr) == 0) { return $entity->id; } $setPart = implode(', ', $setArr); - $wherePart = $this->query->getWhere($entity, array('id' => $entity->id, 'deleted' => 0)); + $wherePart = $this->query->getWhere($entity, ['id' => $entity->id, 'deleted' => 0]); $sql = $this->composeUpdateQuery($this->toDb($entity->getEntityType()), $setPart, $wherePart); @@ -815,21 +817,25 @@ abstract class Mapper implements IMapper return $this->update($entity); } - protected function toArray(IEntity $entity, $onlyStorable = true) + protected function toValueMap(IEntity $entity, $onlyStorable = true) { - $arr = array(); - foreach ($entity->fields as $field => $fieldDefs) { - if ($entity->has($field)) { + $data = []; + foreach ($entity->getAttributes() as $attribute => $defs) { + if ($entity->has($attribute)) { if ($onlyStorable) { - if (!empty($fieldDefs['notStorable']) || !empty($fieldDefs['autoincrement']) || isset($fieldDefs['source']) && $fieldDefs['source'] != 'db') - continue; - if ($fieldDefs['type'] == IEntity::FOREIGN) - continue; + if ( + !empty($defs['notStorable']) + || + !empty($defs['autoincrement']) + || + isset($defs['source']) && $defs['source'] != 'db' + ) continue; + if ($defs['type'] == IEntity::FOREIGN) continue; } - $arr[$field] = $entity->get($field); + $data[$attribute] = $entity->get($attribute); } } - return $arr; + return $data; } protected function fromRow(IEntity $entity, $data) diff --git a/application/Espo/ORM/Entity.php b/application/Espo/ORM/Entity.php index 74fbc3bd07..ddc5687d97 100644 --- a/application/Espo/ORM/Entity.php +++ b/application/Espo/ORM/Entity.php @@ -369,25 +369,83 @@ abstract class Entity implements IEntity return $this->isFetched; } - public function isFieldChanged($fieldName) + public function isFieldChanged($name) { - return $this->has($fieldName) && ($this->get($fieldName) != $this->getFetched($fieldName)); + return $this->has($name) && ($this->get($name) != $this->getFetched($name)); } - public function isAttributeChanged($fieldName) + public function isAttributeChanged($name) { - return $this->has($fieldName) && ($this->get($fieldName) != $this->getFetched($fieldName)); + if (!$this->has($name)) return false; + + if (!$this->hasFetched($name)) { + return true; + } + return !self::areValuesEqual($this->getAttributeType($name), $this->get($name), $this->getFetched($name)); + + return $this->get($name) != $this->getFetched($name); } - public function setFetched($fieldName, $value) + public static function areValuesEqual($type, $v1, $v2) { - $this->fetchedValuesContainer[$fieldName] = $value; + if ($type === self::JSON_ARRAY) { + if (is_array($v1) && is_array($v2)) { + if ($v1 != $v2) { + return false; + } + foreach ($v1 as $i => $itemValue) { + if (is_object($v1[$i]) && is_object($v2[$i])) { + if (!self::areValuesEqual(self::JSON_OBJECT, $v1[$i], $v2[$i])) { + return false; + } + continue; + } + if ($v1[$i] !== $v2[$i]) { + return false; + } + } + return true; + } + } else if ($type === self::JSON_OBJECT) { + if (is_object($v1) && is_object($v2)) { + if ($v1 != $v2) { + return false; + } + $a1 = get_object_vars($v1); + $a2 = get_object_vars($v2); + foreach ($v1 as $key => $itemValue) { + if (is_object($a1[$key]) && is_object($a2[$key])) { + if (!self::areValuesEqual(self::JSON_OBJECT, $a1[$key], $a2[$key])) { + return false; + } + continue; + } + if (is_array($a1[$key]) && is_array($a2[$key])) { + if (!self::areValuesEqual(self::JSON_ARRAY, $a1[$key], $a2[$key])) { + return false; + } + continue; + } + if ($a1[$key] !== $a2[$key]) { + return false; + } + } + return true; + } + } + + return $v1 === $v2; } - public function getFetched($fieldName) + public function setFetched($name, $value) { - if (isset($this->fetchedValuesContainer[$fieldName])) { - return $this->fetchedValuesContainer[$fieldName]; + $this->fetchedValuesContainer[$name] = $value; + } + + public function getFetched($name) + { + if (isset($this->fetchedValuesContainer[$name])) { + return $this->fetchedValuesContainer[$name]; } return null; } diff --git a/tests/unit/Espo/ORM/DB/MapperTest.php b/tests/unit/Espo/ORM/DB/MapperTest.php index f960067e85..a3bed51b7d 100644 --- a/tests/unit/Espo/ORM/DB/MapperTest.php +++ b/tests/unit/Espo/ORM/DB/MapperTest.php @@ -35,6 +35,7 @@ use Espo\Entities\Post; use Espo\Entities\Comment; use Espo\Entities\Tag; use Espo\Entities\Note; +use Espo\Entities\Job; require_once 'tests/unit/testData/DB/Entities.php'; require_once 'tests/unit/testData/DB/MockPDO.php'; @@ -348,6 +349,36 @@ class DBMapperTest extends \PHPUnit\Framework\TestCase $this->db->update($this->post); } + public function testUpdateArray1() + { + $query = "UPDATE `job` SET `array` = '[\"2\",\"1\"]' WHERE job.id = '1' AND job.deleted = '0'"; + + $this->mockQuery($query, true); + + $job = new \Espo\Entities\Job(); + $job->id = '1'; + $job->setFetched('array', ['1', '2']); + $job->set('array', ['2', '1']); + + $this->db->update($job); + + + } + + public function testUpdateArray2() + { + $query = "UPDATE `job` SET `array` = NULL WHERE job.id = '1' AND job.deleted = '0'"; + + $this->mockQuery($query, true); + + $job = new \Espo\Entities\Job(); + $job->id = '1'; + $job->setFetched('array', ['1', '2']); + $job->set('array', null); + + $this->db->update($job); + } + public function testRemoveRelationHasMany() { $query = "UPDATE `comment` SET post_id = NULL WHERE comment.deleted = '0' AND comment.id = '100'"; @@ -448,6 +479,5 @@ class DBMapperTest extends \PHPUnit\Framework\TestCase ) )); } + } - - diff --git a/tests/unit/Espo/ORM/EntityTest.php b/tests/unit/Espo/ORM/EntityTest.php new file mode 100644 index 0000000000..53781f0118 --- /dev/null +++ b/tests/unit/Espo/ORM/EntityTest.php @@ -0,0 +1,181 @@ +setFetched('string', 'test'); + $this->assertFalse($job->isAttributeChanged('string')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('string', 'test'); + $job->set('string', 'hello'); + $this->assertTrue($job->isAttributeChanged('string')); + + $job = new \Espo\Entities\Job(); + $job->set('string', 'hello'); + $this->assertTrue($job->isAttributeChanged('string')); + + $job = new \Espo\Entities\Job(); + $job->set('string', null); + $this->assertTrue($job->isAttributeChanged('string')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', ['1', '2']); + $job->set('array', ['2', '1']); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', ['1', '2']); + $job->set('array', ['1', '2']); + $this->assertFalse($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', ['1', '2']); + $job->set('array', ['1', 2]); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', [ + (object) ['1' => 'v1'] + ]); + $job->set('array', [ + (object) ['1' => 'v1'] + ]); + $this->assertFalse($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', [ + (object) ['k1' => 'v1'] + ]); + $job->set('array', [ + (object) ['k1' => 'v2'] + ]); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', [ + (object) ['k1' => 'v1'] + ]); + $job->set('array', [ + (object) ['k1' => 'v1', 'k2' => 'v2'], + ]); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', ['1', '2']); + $job->set('array', ['1', '2', '3']); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->set('array', ['1', '2', '3']); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->set('array', null); + $this->assertTrue($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('array', null); + $this->assertFalse($job->isAttributeChanged('array')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('object', (object) ['1' => 'value-1']); + $job->set('object', (object) ['1' => 'value-1']); + $this->assertFalse($job->isAttributeChanged('object')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('object', (object) ['1' => 'value-1']); + $job->set('object', ['1' => 'value-1']); + $this->assertTrue($job->isAttributeChanged('object')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('object', (object) ['1' => '1']); + $job->set('object', (object) ['1' => 1]); + $this->assertTrue($job->isAttributeChanged('object')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('object', (object) [ + 'k1' => (object) [ + 'k11' => 'v1' + ] + ]); + $job->set('object', (object) [ + 'k1' => (object) [ + 'k11' => 'v2' + ] + ]); + $this->assertTrue($job->isAttributeChanged('object')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('object', (object) [ + 'k1' => [ + 'k11' => 'v1' + ] + ]); + $job->set('object', (object) [ + 'k1' => (object) [ + 'k11' => 'v1' + ] + ]); + $this->assertTrue($job->isAttributeChanged('object')); + + $job = new \Espo\Entities\Job(); + $job->setFetched('object', [ + 'k1' => [ + 'k11' => 'v1' + ] + ]); + $job->set('object', (object) [ + 'k1' => (object) [ + 'k11' => 'v1' + ] + ]); + $this->assertTrue($job->isAttributeChanged('object')); + } + +} diff --git a/tests/unit/testData/DB/Entities.php b/tests/unit/testData/DB/Entities.php index 93117edf1c..c48db06993 100644 --- a/tests/unit/testData/DB/Entities.php +++ b/tests/unit/testData/DB/Entities.php @@ -242,3 +242,28 @@ class Article extends TEntity ) ); } + + +class Job extends TEntity +{ + public $fields = array( + 'id' => array( + 'type' => Entity::ID + ), + 'string' => array( + 'type' => Entity::VARCHAR, + 'len' => 50 + ), + 'array' => array( + 'type' => Entity::JSON_ARRAY + ), + 'object' => array( + 'type' => Entity::JSON_OBJECT + ), + 'deleted' => array( + 'type' => Entity::BOOL, + 'default' => 0 + ) + ); +} +