diff --git a/application/Espo/ORM/DB/BaseMapper.php b/application/Espo/ORM/DB/BaseMapper.php index a795557d11..23cf06035b 100644 --- a/application/Espo/ORM/DB/BaseMapper.php +++ b/application/Espo/ORM/DB/BaseMapper.php @@ -1053,26 +1053,17 @@ abstract class BaseMapper implements Mapper protected function insertInternal(Entity $entity, ?array $onDuplicateUpdateAttributeList = null) { - $dataList = $this->toValueMap($entity); - - $columnList = $this->getInsertColumnList($entity); - $valueList = $this->getInsertValueList($entity); - - $fieldsPart = "`" . implode("`, `", $columnList) . "`"; - $valuesPart = implode(", ", $valueList); - - $onDuplicatePart = null; + $update = null; if ($onDuplicateUpdateAttributeList && count($onDuplicateUpdateAttributeList)) { - $onDuplicateSetMap = $this->getInsertOnDuplicateSetMap($entity, $onDuplicateUpdateAttributeList); - $onDuplicateSubPartList = []; - foreach ($onDuplicateSetMap as $attribute => $value) { - $onDuplicateSubPartList[] = "`" . $this->toDb($attribute) . "` = " . $this->quote($value); - } - $onDuplicatePart = implode(', ', $onDuplicateSubPartList); + $update = $onDuplicateSetMap = $this->getInsertOnDuplicateSetMap($entity, $onDuplicateUpdateAttributeList); } - $sql = $this->composeInsertQuery($this->toDb($entity->getEntityType()), $fieldsPart, $valuesPart, $onDuplicatePart); + $sql = $this->query->createInsertQuery($entity->getEntityType(), [ + 'columns' => $this->getInsertColumnList($entity), + 'values' => $this->getInsertValueMap($entity), + 'update' => $update, + ]); $this->runQuery($sql, true); } @@ -1086,25 +1077,16 @@ abstract class BaseMapper implements Mapper return; } - $columnList = $this->getInsertColumnList($collection[0]); - - $fieldsPart = "`" . implode("`, `", $columnList) . "`"; - - $valuesPartList = []; - - $entityType = $collection[0]->getEntityType(); + $values = []; foreach ($collection as $entity) { - if ($entity->getEntityType() != $entityType) { - throw new Error("Mapper: Can't mass insert collection of different entity types."); - } - - $valueList = $this->getInsertValueList($entity); - $valuesPart = implode(", ", $valueList); - $valuesPartList[] = $valuesPart; + $values[] = $this->getInsertValueMap($entity); } - $sql = $this->composeInsertQuery($this->toDb($entityType), $fieldsPart, $valuesPartList); + $sql = $this->query->createInsertQuery($entity->getEntityType(), [ + 'columns' => $this->getInsertColumnList($collection[0]), + 'values' => $values, + ]); $this->runQuery($sql, true); } @@ -1116,25 +1098,22 @@ abstract class BaseMapper implements Mapper $dataList = $this->toValueMap($entity); foreach ($dataList as $attribute => $value) { - $columnList[] = $this->toDb($attribute); + $columnList[] = $attribute; } return $columnList; } - protected function getInsertValueList(Entity $entity) : array + protected function getInsertValueMap(Entity $entity) : array { - $valueList = []; + $map = []; - $dataList = $this->toValueMap($entity); - - foreach ($dataList as $attribute => $value) { + foreach ($this->toValueMap($entity) as $attribute => $value) { $type = $entity->getAttributeType($attribute); - $value = $this->prepareValueForInsert($type, $value); - $valueList[] = $this->quote($value); + $map[$attribute] = $this->prepareValueForInsert($type, $value); } - return $valueList; + return $map; } protected function getInsertOnDuplicateSetMap(Entity $entity, array $attributeList) @@ -1189,7 +1168,8 @@ abstract class BaseMapper implements Mapper 'id' => $entity->id, 'deleted' => false, ], - ], $valueMap); + 'update' => $valueMap, + ]); $this->pdo->query($sql); } @@ -1246,7 +1226,10 @@ abstract class BaseMapper implements Mapper 'id' => $id, ]; - $sql = $this->query->createUpdateQuery($entityType, ['whereClause' => $whereClause], ['deleted' => false]); + $sql = $this->query->createUpdateQuery($entityType, [ + 'whereClause' => $whereClause, + 'update' => ['deleted' => false], + ]); $this->runQuery($sql); } diff --git a/application/Espo/ORM/DB/Query/BaseQuery.php b/application/Espo/ORM/DB/Query/BaseQuery.php index 34f281c327..9adb33d4d2 100644 --- a/application/Espo/ORM/DB/Query/BaseQuery.php +++ b/application/Espo/ORM/DB/Query/BaseQuery.php @@ -44,7 +44,7 @@ use PDO; */ abstract class BaseQuery { - protected static $selectParamList = [ + protected static $paramList = [ 'select', 'whereClause', 'offset', @@ -66,6 +66,7 @@ abstract class BaseQuery 'maxTextColumnsLength', 'useIndex', 'withDeleted', + 'update', ]; protected static $sqlOperators = [ @@ -269,6 +270,7 @@ abstract class BaseQuery const SELECT_METHOD = 'SELECT'; const DELETE_METHOD = 'DELETE'; const UPDATE_METHOD = 'UPDATE'; + const INSERT_METHOD = 'INESRT'; protected $entityFactory; @@ -332,10 +334,12 @@ abstract class BaseQuery /** * Compose an UPDATE query. */ - public function createUpdateQuery(string $entityType, ?array $params = null, array $values) : string + public function createUpdateQuery(string $entityType, ?array $params = null) : string { $params = $this->normilizeParams(self::UPDATE_METHOD, $params); + $values = $params['update']; + $entity = $this->getSeed($entityType); $wherePart = $this->getWherePart($entity, $params['whereClause'], 'AND', $params); @@ -356,11 +360,85 @@ abstract class BaseQuery return $sql; } + public function createInsertQuery(string $entityType, array $params) : string + { + $params = $this->normilizeInsertParams($params); + + $columns = $params['columns']; + $values = $params['values']; + $isMass = $params['isMass']; + $update = $params['update']; + + $columnsPart = $this->getInsertColumnsPart($columns); + + if ($isMass) { + $valuesPart = []; + foreach ($values as $item) { + $valuesPart[] = $this->getInsertValuesPart($columns, $item); + } + } else { + $valuesPart = $this->getInsertValuesPart($columns, $values); + } + + $updatePart = null; + + if ($update) { + $updatePart = $this->getInsertUpdatePart($update); + } + + return $this->composeInsertQuery($this->toDb($entityType), $columnsPart, $valuesPart, $updatePart); + } + + protected function normilizeInsertParams(array $params) : array + { + $columns = $params['columns'] ?? null; + + if (empty($columns) || !is_array($columns)) { + throw new Error("ORM Query: 'columns' is empty for INSERT."); + } + + $params['isMass'] = $params['isMass'] ?? false; + + $values = $params['values'] ?? null; + + if (empty($values) || !is_array($values)) { + throw new Error("ORM Query: 'values' is empty for INSERT."); + } + + $isMass = array_keys($values)[0] === 0; + + $params['isMass'] = $isMass; + + if (!$isMass) { + foreach ($columns as $item) { + if (!array_key_exists($item, $values)) { + throw new Error("ORM Query: 'values' should contain all items listed in 'columns'."); + } + } + } else { + foreach ($values as $valuesItem) { + foreach ($columns as $item) { + if (!array_key_exists($item, $valuesItem)) { + throw new Error("ORM Query: 'values' should contain all items listed in 'columns'."); + } + } + } + } + + $update = $params['update'] = $params['update'] ?? null; + + if ($update && !is_array($update)) { + throw new Error("ORM Query: Bad 'update' param."); + } + + return $params; + } + protected function normilizeParams(string $method, ?array $params) : array { $params = $params ?? []; - foreach (self::$selectParamList as $k) { + foreach (self::$paramList as $k) { $params[$k] = array_key_exists($k, $params) ? $params[$k] : null; } @@ -373,14 +451,24 @@ abstract class BaseQuery if ($method !== self::SELECT_METHOD) { if (isset($params['aggregation'])) { - throw new Error("ORM Query: Aggregation is not allowed for '{$method}'."); + throw new Error("ORM Query: Param 'aggregation' is not allowed for '{$method}'."); } if (isset($params['offset'])) { - throw new Error("ORM Query: Offset is not allowed for '{$method}'."); + throw new Error("ORM Query: Param 'offset' is not allowed for '{$method}'."); } } + if ($method !== self::UPDATE_METHOD && $method !== self::INSERT_METHOD) { + if (isset($params['update'])) { + throw new Error("ORM Query: Param 'update' is not allowed for '{$method}'."); + } + } + + if (isset($params['update']) && !is_array($params['update'])) { + throw new Error("ORM Query: Param 'update' should be an array."); + } + return $params; } @@ -2336,6 +2424,25 @@ abstract class BaseQuery return $sql; } + protected function composeInsertQuery(string $table, string $columns, $values, ?string $update = null) : string + { + $sql = "INSERT INTO `{$table}`"; + + $sql .= " ({$columns})"; + + if (is_array($values)) { + $sql .= " VALUES (" . implode("), (", $values) . ")"; + } else { + $sql .= " VALUES ({$values})"; + } + + if ($update) { + $sql .= " ON DUPLICATE KEY UPDATE " . $update; + } + + return $sql; + } + protected function getSetPart(Entity $entity, array $values) : string { if (!count($values)) { @@ -2375,6 +2482,39 @@ abstract class BaseQuery return implode(', ', $list); } + protected function getInsertColumnsPart(array $columnList) : string + { + $list = []; + + foreach ($columnList as $column) { + $list[] = '`'.$this->toDb($this->sanitize($column)) . '`'; + } + + return implode(', ', $list); + } + + protected function getInsertValuesPart(array $columnList, array $values) : string + { + $list = []; + + foreach ($columnList as $column) { + $list[] = $this->quote($values[$column]); + } + + return implode(', ', $list); + } + + protected function getInsertUpdatePart(array $values) : string + { + $list = []; + + foreach ($values as $column => $value) { + $list[] = "`" . $this->toDb($this->sanitize($column)) . "` = " . $this->quote($value); + } + + return implode(', ', $list); + } + /** * Add a LIMIT part to a SQL query. */ diff --git a/application/Espo/ORM/RDBQueryExecutor.php b/application/Espo/ORM/RDBQueryExecutor.php index c99ff44fb4..8d5a969dc7 100644 --- a/application/Espo/ORM/RDBQueryExecutor.php +++ b/application/Espo/ORM/RDBQueryExecutor.php @@ -45,7 +45,10 @@ class RDBQueryExecutor public function update(RDBSelect $select, array $values) { - $sql = $this->entityManager->getQuery()->createUpdateQuery($select->getEntityType(), $select->getRawParams(), $values); + $params = $select->getRawParams(); + $params['update'] = $values; + + $sql = $this->entityManager->getQuery()->createUpdateQuery($select->getEntityType(), $params); $this->entityManager->runQuery($sql, true); } diff --git a/application/Espo/Repositories/EmailAddress.php b/application/Espo/Repositories/EmailAddress.php index 870ce020be..11f17805bb 100644 --- a/application/Espo/Repositories/EmailAddress.php +++ b/application/Espo/Repositories/EmailAddress.php @@ -583,11 +583,7 @@ class EmailAddress extends \Espo\Core\Repositories\Database implements ]) ->build(); - $sql = $this->getEntityManager()->getQuery()->createUpdateQuery('EntityEmailAddress', $updateSelect->getRawParams(), [ - 'primary' => true, - ]); - - $this->getEntityManager()->runQuery($sql, true); + $this->getEntityManager()->getQueryExecutor()->update($updateSelect, ['primary' => true]); } else { if ( diff --git a/application/Espo/Repositories/PhoneNumber.php b/application/Espo/Repositories/PhoneNumber.php index 755aa23a36..6e7efd2d06 100644 --- a/application/Espo/Repositories/PhoneNumber.php +++ b/application/Espo/Repositories/PhoneNumber.php @@ -511,11 +511,7 @@ class PhoneNumber extends \Espo\Core\Repositories\Database implements ]) ->build(); - $sql = $this->getEntityManager()->getQuery()->createUpdateQuery('EntityPhoneNumber', $updateSelect->getRawParams(), [ - 'primary' => true, - ]); - - $this->getEntityManager()->runQuery($sql, true); + $this->getEntityManager()->getQueryExecutor()->update($updateSelect, ['primary' => true]); } else { if ( diff --git a/application/Espo/Repositories/Preferences.php b/application/Espo/Repositories/Preferences.php index b90a70c17c..3731876640 100644 --- a/application/Espo/Repositories/Preferences.php +++ b/application/Espo/Repositories/Preferences.php @@ -72,22 +72,6 @@ class Preferences extends Repository implements Removable, if (!isset($this->data[$id])) { $this->loadData($id); - - /*$pdo = $this->entityManager->getPDO(); - $sql = "SELECT `id`, `data` FROM `preferences` WHERE id = ".$pdo->quote($id); - $ps = $pdo->query($sql);*/ - - - /*$sth = $pdo->prepare($sql); - $sth->execute(); - - while ($row = $sth->fetch(\PDO::FETCH_ASSOC)) { - $data = Json::decode($row['data']); - $data = get_object_vars($data); - break; - }*/ - - } $entity->set($this->data[$id]); @@ -226,10 +210,16 @@ class Preferences extends Repository implements Removable, $pdo = $this->entityManager->getPDO(); - $sql = " - INSERT INTO `preferences` (`id`, `data`) VALUES (".$pdo->quote($entity->id).", ".$pdo->quote($dataString).") - ON DUPLICATE KEY UPDATE `data` = ".$pdo->quote($dataString)." - "; + $sql = $this->entityManager->getQuery()->createInsertQuery('Preferences', [ + 'columns' => ['id', 'data'], + 'values' => [ + 'id' => $entity->id, + 'data' => $dataString, + ], + 'update' => [ + 'data' => $dataString, + ], + ]); $pdo->query($sql); @@ -244,14 +234,22 @@ class Preferences extends Repository implements Removable, public function deleteFromDb(string $id) { $pdo = $this->entityManager->getPDO(); - $sql = "DELETE FROM `preferences` WHERE `id` = " . $pdo->quote($id); - $ps = $pdo->query($sql); + + $sql = $this->entityManager->getQuery()->createDeleteQuery('Preferences', [ + 'whereClause' => [ + 'id' => $id, + ], + ]); + + $pdo->query($sql); } public function remove(Entity $entity, array $options = []) { if (!$entity->id) return; + $this->deleteFromDb($entity->id); + if (isset($this->data[$entity->id])) { unset($this->data[$entity->id]); } @@ -260,9 +258,11 @@ class Preferences extends Repository implements Removable, public function resetToDefaults(string $userId) { $this->deleteFromDb($userId); + if (isset($this->data[$userId])) { unset($this->data[$userId]); } + if ($entity = $this->get($userId)) { return $entity->toArray(); } diff --git a/tests/unit/Espo/ORM/DB/QueryTest.php b/tests/unit/Espo/ORM/DB/QueryTest.php index a30ed5837f..a2992dcea8 100644 --- a/tests/unit/Espo/ORM/DB/QueryTest.php +++ b/tests/unit/Espo/ORM/DB/QueryTest.php @@ -160,9 +160,10 @@ class QueryTest extends \PHPUnit\Framework\TestCase 'whereClause' => [ 'name' => 'test', ], - ], [ - 'deleted' => false, - 'name' => 'hello', + 'update' => [ + 'deleted' => false, + 'name' => 'hello', + ], ]); $expectedSql = @@ -180,8 +181,9 @@ class QueryTest extends \PHPUnit\Framework\TestCase 'name' => 'test', ], 'joins' => ['post'], - ], [ - 'name:' => 'post.name', + 'update' => [ + 'name:' => 'post.name', + ], ]); $expectedSql = @@ -200,9 +202,10 @@ class QueryTest extends \PHPUnit\Framework\TestCase 'name' => 'test', ], 'orderBy' => 'name', - ], [ - 'deleted' => false, - 'name' => 'hello', + 'update' => [ + 'deleted' => false, + 'name' => 'hello', + ] ]); $expectedSql = @@ -222,9 +225,10 @@ class QueryTest extends \PHPUnit\Framework\TestCase ], 'orderBy' => 'name', 'limit' => 1, - ], [ - 'deleted' => false, - 'name' => 'hello', + 'update' => [ + 'deleted' => false, + 'name' => 'hello', + ], ]); $expectedSql = @@ -237,6 +241,64 @@ class QueryTest extends \PHPUnit\Framework\TestCase $this->assertEquals($expectedSql, $sql); } + public function testInsertQuery1() + { + $sql = $this->query->createInsertQuery('Account', [ + 'columns' => ['id', 'name'], + 'values' => [ + 'id' => '1', + 'name' => 'hello', + ], + ]); + + $expectedSql = + "INSERT INTO `account` (`id`, `name`) VALUES ('1', 'hello')"; + + $this->assertEquals($expectedSql, $sql); + } + + public function testInsertQuery2() + { + $sql = $this->query->createInsertQuery('Account', [ + 'columns' => ['id', 'name'], + 'values' => [ + [ + 'id' => '1', + 'name' => 'hello', + ], + [ + 'id' => '2', + 'name' => 'test', + ], + ], + ]); + + $expectedSql = + "INSERT INTO `account` (`id`, `name`) VALUES ('1', 'hello'), ('2', 'test')"; + + $this->assertEquals($expectedSql, $sql); + } + + public function testInsertUpdate() + { + $sql = $this->query->createInsertQuery('Account', [ + 'columns' => ['id', 'name'], + 'values' => [ + 'id' => '1', + 'name' => 'hello', + ], + 'update' => [ + 'deleted' => false, + 'name' => 'test' + ], + ]); + + $expectedSql = + "INSERT INTO `account` (`id`, `name`) VALUES ('1', 'hello') ON DUPLICATE KEY UPDATE `deleted` = 0, `name` = 'test'"; + + $this->assertEquals($expectedSql, $sql); + } + public function testSelectAllColumns() { $sql = $this->query->createSelectQuery('Account', [