From 77ba94686163c02709ff884d191398cce3a7252e Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Wed, 5 Aug 2020 14:12:49 +0300 Subject: [PATCH] orm refactoring --- application/Espo/ORM/DB/BaseMapper.php | 45 +++--- application/Espo/ORM/DB/Helper.php | 163 +++++++++++++++++++ application/Espo/ORM/DB/Query/BaseQuery.php | 168 +++++--------------- application/Espo/ORM/EntityManager.php | 2 + 4 files changed, 230 insertions(+), 148 deletions(-) create mode 100644 application/Espo/ORM/DB/Helper.php diff --git a/application/Espo/ORM/DB/BaseMapper.php b/application/Espo/ORM/DB/BaseMapper.php index ce022e6129..52d027ba1d 100644 --- a/application/Espo/ORM/DB/BaseMapper.php +++ b/application/Espo/ORM/DB/BaseMapper.php @@ -29,8 +29,6 @@ namespace Espo\ORM\DB; -use Espo\Core\Exceptions\Error; - use Espo\ORM\{ Entity, Collection, @@ -44,6 +42,7 @@ use Espo\ORM\{ use PDO; use Exception; use LogicException; +use RuntimeException; /** * Abstraction for DB. Mapping of Entity to DB. Supposed to be used only internally. Use repositories instead. @@ -72,6 +71,8 @@ abstract class BaseMapper implements Mapper $this->query = $query; $this->entityFactory = $entityFactory; $this->metadata = $metadata; + + $this->helper = new Helper($metadata); } /** @@ -239,7 +240,7 @@ abstract class BaseMapper implements Mapper $relType = $relDefs['type']; - $keySet = $this->query->getKeys($entity, $relationName); + $keySet = $this->helper->getRelationKeys($entity, $relationName); $key = $keySet['key']; $foreignKey = $keySet['foreignKey']; @@ -456,7 +457,7 @@ abstract class BaseMapper implements Mapper public function updateRelation(Entity $entity, string $relationName, ?string $id = null, array $columnData) : bool { if (empty($id) || empty($relationName)) { - throw new Error("Can't update relation, empty ID or relation name."); + throw new RuntimeException("Can't update relation, empty ID or relation name."); } if (empty($columnData)) { @@ -464,7 +465,7 @@ abstract class BaseMapper implements Mapper } $relDefs = $entity->getRelations()[$relationName]; - $keySet = $this->query->getKeys($entity, $relationName); + $keySet = $this->helper->getRelationKeys($entity, $relationName); $relType = $relDefs['type']; @@ -520,16 +521,16 @@ abstract class BaseMapper implements Mapper $type = $entity->getRelationType($relationName); if (!$type === Entity::MANY_MANY) { - throw new Error("'getRelationColumn' works only on many-to-many relations."); + throw new RuntimeException("'getRelationColumn' works only on many-to-many relations."); } if (!$id) { - throw new Error("Empty ID passed to 'getRelationColumn'."); + throw new RuntimeException("Empty ID passed to 'getRelationColumn'."); } $middleName = ucfirst($entity->getRelationParam($relationName, 'relationName')); - $keySet = $this->query->getKeys($entity, $relationName); + $keySet = $this->helper->getRelationKeys($entity, $relationName); $nearKey = $keySet['nearKey']; $distantKey = $keySet['distantKey']; @@ -594,7 +595,7 @@ abstract class BaseMapper implements Mapper $id = $entity->id; if (empty($id) || empty($relationName)) { - throw new Error("Cant't mass relate on empty ID or relation name."); + throw new RuntimeException("Cant't mass relate on empty ID or relation name."); } $relDefs = $entity->getRelations()[$relationName]; @@ -609,7 +610,7 @@ abstract class BaseMapper implements Mapper $relEntity = $this->entityFactory->create($foreignEntityType); - $keySet = $this->query->getKeys($entity, $relationName); + $keySet = $this->helper->getRelationKeys($entity, $relationName); switch ($relType) { case Entity::MANY_MANY: @@ -686,11 +687,11 @@ abstract class BaseMapper implements Mapper } if (empty($id) || empty($relationName) || !$entity->get('id')) { - throw new Error("Can't relate an empty entity or relation name."); + throw new RuntimeException("Can't relate an empty entity or relation name."); } if (!$entity->hasRelation($relationName)) { - throw new Error("Relation '{$relationName}' does not exist in '{$entityType}'."); + throw new RuntimeException("Relation '{$relationName}' does not exist in '{$entityType}'."); } $relDefs = $entity->getRelations()[$relationName]; @@ -698,7 +699,7 @@ abstract class BaseMapper implements Mapper $relType = $entity->getRelationType($relationName); if ($relType == Entity::BELONGS_TO_PARENT && !$relEntity) { - throw new Error("Bad foreign passed."); + throw new RuntimeException("Bad foreign passed."); } $foreignEntityType = $entity->getRelationParam($relationName, 'entity'); @@ -712,7 +713,7 @@ abstract class BaseMapper implements Mapper $relEntity->id = $id; } - $keySet = $this->query->getKeys($entity, $relationName); + $keySet = $this->helper->getRelationKeys($entity, $relationName); switch ($relType) { case Entity::BELONGS_TO: @@ -923,11 +924,11 @@ abstract class BaseMapper implements Mapper $entityType = $entity->getEntityType(); if (empty($id) && empty($all) || empty($relationName)) { - throw new Error("Can't unrelate an empty entity or relation name."); + throw new RuntimeException("Can't unrelate an empty entity or relation name."); } if (!$entity->hasRelation($relationName)) { - throw new Error("Relation '{$relationName}' does not exist in '{$entityType}'."); + throw new RuntimeException("Relation '{$relationName}' does not exist in '{$entityType}'."); } $relDefs = $entity->getRelations()[$relationName]; @@ -935,7 +936,7 @@ abstract class BaseMapper implements Mapper $relType = $entity->getRelationType($relationName); if ($relType === Entity::BELONGS_TO_PARENT && !$relEntity && !$all) { - throw new Error("Bad foreign passed."); + throw new RuntimeException("Bad foreign passed."); } $foreignEntityType = $entity->getRelationParam($relationName, 'entity'); @@ -955,7 +956,7 @@ abstract class BaseMapper implements Mapper $relEntity->id = $id; } - $keySet = $this->query->getKeys($entity, $relationName); + $keySet = $this->helper->getRelationKeys($entity, $relationName); switch ($relType) { case Entity::BELONGS_TO: @@ -1233,7 +1234,7 @@ abstract class BaseMapper implements Mapper public function deleteFromDb(string $entityType, string $id, bool $onlyDeleted = false) { if (empty($entityType) || empty($id)) { - throw new Error("Can't delete an empty entity type or ID from DB."); + throw new RuntimeException("Can't delete an empty entity type or ID from DB."); } $whereClause = [ @@ -1257,7 +1258,7 @@ abstract class BaseMapper implements Mapper public function restoreDeleted(string $entityType, string $id) { if (empty($entityType) || empty($id)) { - throw new Error("Can't restore an empty entity type or ID."); + throw new RuntimeException("Can't restore an empty entity type or ID."); } $whereClause = [ @@ -1318,7 +1319,7 @@ abstract class BaseMapper implements Mapper $middleName = $defs['relationName'] ?? null; - $keySet = $this->query->getKeys($entity, $relationName); + $keySet = $this->helper->getRelationKeys($entity, $relationName); $key = $keySet['key']; $foreignKey = $keySet['foreignKey']; @@ -1326,7 +1327,7 @@ abstract class BaseMapper implements Mapper $distantKey = $keySet['distantKey']; if (!$middleName) { - throw new Error("No 'relationName' parameter for '{$relationName}' relationship."); + throw new RuntimeException("No 'relationName' parameter for '{$relationName}' relationship."); } $alias = lcfirst($middleName); diff --git a/application/Espo/ORM/DB/Helper.php b/application/Espo/ORM/DB/Helper.php new file mode 100644 index 0000000000..c66ea4f425 --- /dev/null +++ b/application/Espo/ORM/DB/Helper.php @@ -0,0 +1,163 @@ +metadata = $metadata; + } + + public function getRelationKeys(Entity $entity, string $relationName) : array + { + if (!$entity->hasRelation($relationName)) { + throw new RuntimeException("Relation '{$relationName}' does not exist."); + } + + $params = $entity->getRelations()[$relationName]; + + $type = $params['type'] ?? null; + + switch ($type) { + + case Entity::BELONGS_TO: + $key = lcfirst($entity->getEntityType()) . 'Id'; + + if (isset($params['key'])) { + $key = $params['key']; + } + + $foreignKey = 'id'; + + if (isset($params['foreignKey'])){ + $foreignKey = $params['foreignKey']; + } + + return [ + 'key' => $key, + 'foreignKey' => $foreignKey, + ]; + + case Entity::HAS_MANY: + case Entity::HAS_ONE: + $key = 'id'; + + if (isset($params['key'])){ + $key = $params['key']; + } + + $foreignKey = lcfirst($entity->getEntityType()) . 'Id'; + + if (isset($params['foreignKey'])) { + $foreignKey = $params['foreignKey']; + } + + return [ + 'key' => $key, + 'foreignKey' => $foreignKey, + ]; + + case Entity::HAS_CHILDREN: + $key = 'id'; + + if (isset($params['key'])){ + $key = $params['key']; + } + + $foreignKey = 'parentId'; + + if (isset($params['foreignKey'])) { + $foreignKey = $params['foreignKey']; + } + + $foreignType = 'parentType'; + + if (isset($params['foreignType'])) { + $foreignType = $params['foreignType']; + } + + return [ + 'key' => $key, + 'foreignKey' => $foreignKey, + 'foreignType' => $foreignType, + ]; + + case Entity::MANY_MANY: + $key = 'id'; + + if (isset($params['key'])){ + $key = $params['key']; + } + + $foreignKey = 'id'; + + if (isset($params['foreignKey'])){ + $foreignKey = $params['foreignKey']; + } + + $nearKey = lcfirst($entity->getEntityType()) . 'Id'; + $distantKey = lcfirst($params['entity']) . 'Id'; + + if (isset($params['midKeys']) && is_array($params['midKeys'])){ + $nearKey = $params['midKeys'][0]; + $distantKey = $params['midKeys'][1]; + } + + return [ + 'key' => $key, + 'foreignKey' => $foreignKey, + 'nearKey' => $nearKey, + 'distantKey' => $distantKey + ]; + + case Entity::BELONGS_TO_PARENT: + $key = $relationName . 'Id'; + $typeKey = $relationName . 'Type'; + + return [ + 'key' => $key, + 'typeKey' => $typeKey, + 'foreignKey' => 'id', + ]; + } + + throw new RuntimeException("Relation type '{$type}' not supported for 'getKeys'."); + } +} diff --git a/application/Espo/ORM/DB/Query/BaseQuery.php b/application/Espo/ORM/DB/Query/BaseQuery.php index 50f2f909da..839c898d7d 100644 --- a/application/Espo/ORM/DB/Query/BaseQuery.php +++ b/application/Espo/ORM/DB/Query/BaseQuery.php @@ -29,15 +29,16 @@ namespace Espo\ORM\DB\Query; -use Espo\Core\Exceptions\Error; use Espo\ORM\{ Entity, EntityFactory, Metadata, + DB\Helper, }; use PDO; +use RuntimeException; /** * Composes SQL queries. @@ -278,6 +279,8 @@ abstract class BaseQuery protected $metadata; + protected $helper; + protected $attributeDbMapCache = []; protected $aliasesCache = []; @@ -289,6 +292,8 @@ abstract class BaseQuery $this->entityFactory = $entityFactory; $this->pdo = $pdo; $this->metadata = $metadata; + + $this->helper = new Helper($metadata); } protected function getSeed(string $entityType) : Entity @@ -410,7 +415,7 @@ abstract class BaseQuery $columns = $params['columns'] ?? null; if (empty($columns) || !is_array($columns)) { - throw new Error("ORM Query: 'columns' is empty for INSERT."); + throw new RuntimeException("ORM Query: 'columns' is empty for INSERT."); } $values = $params['values'] = $params['values'] ?? null; @@ -425,16 +430,16 @@ abstract class BaseQuery if (!$isBySelect) { if (empty($values) || !is_array($values)) { - throw new Error("ORM Query: 'values' is empty for INSERT."); + throw new RuntimeException("ORM Query: 'values' is empty for INSERT."); } } if ($isBySelect) { if (!is_array($valuesSelectParams)) { - throw new Error("ORM Query: Bad 'valuesSelectParams' parameter."); + throw new RuntimeException("ORM Query: Bad 'valuesSelectParams' parameter."); } if (!isset($valuesSelectParams['from'])) { - throw new Error("ORM Query: Missing 'from' in 'valuesSelectParams'."); + throw new RuntimeException("ORM Query: Missing 'from' in 'valuesSelectParams'."); } } @@ -448,14 +453,14 @@ abstract class BaseQuery 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'."); + throw new RuntimeException("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'."); + throw new RuntimeException("ORM Query: 'values' should contain all items listed in 'columns'."); } } } @@ -465,7 +470,7 @@ abstract class BaseQuery $update = $params['update'] = $params['update'] ?? null; if ($update && !is_array($update)) { - throw new Error("ORM Query: Bad 'update' param."); + throw new RuntimeException("ORM Query: Bad 'update' param."); } return $params; @@ -488,22 +493,22 @@ abstract class BaseQuery if ($method !== self::SELECT_METHOD) { if (isset($params['aggregation'])) { - throw new Error("ORM Query: Param 'aggregation' is not allowed for '{$method}'."); + throw new RuntimeException("ORM Query: Param 'aggregation' is not allowed for '{$method}'."); } if (isset($params['offset'])) { - throw new Error("ORM Query: Param 'offset' is not allowed for '{$method}'."); + throw new RuntimeException("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}'."); + throw new RuntimeException("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."); + throw new RuntimeException("ORM Query: Param 'update' should be an array."); } return $params; @@ -749,7 +754,7 @@ abstract class BaseQuery string $function, string $part, string $entityType, bool $distinct = false, ?array $argumentPartList = null ) : string { if (!in_array($function, $this->functionList)) { - throw new Error("ORM Query: Not allowed function '{$function}'."); + throw new RuntimeException("ORM Query: Not allowed function '{$function}'."); } if (strpos($function, 'YEAR_') === 0 && $function !== 'YEAR_NUMBER') { @@ -785,7 +790,7 @@ abstract class BaseQuery if (in_array($function, $this->comparisonFunctionList)) { if (count($argumentPartList) < 2) { - throw new Error("ORM Query: Not enough arguments for function '{$function}'."); + throw new RuntimeException("ORM Query: Not enough arguments for function '{$function}'."); } $operator = $this->comparisonFunctionOperatorMap[$function]; return $argumentPartList[0] . ' ' . $operator . ' ' . $argumentPartList[1]; @@ -793,7 +798,7 @@ abstract class BaseQuery if (in_array($function, $this->mathOperationFunctionList)) { if (count($argumentPartList) < 2) { - throw new Error("ORM Query: Not enough arguments for function '{$function}'."); + throw new RuntimeException("ORM Query: Not enough arguments for function '{$function}'."); } $operator = $this->mathFunctionOperatorMap[$function]; return '(' . implode(' ' . $operator . ' ', $argumentPartList) . ')'; @@ -803,7 +808,7 @@ abstract class BaseQuery $operator = $this->comparisonFunctionOperatorMap[$function]; if (count($argumentPartList) < 2) { - throw new Error("ORM Query: Not enough arguments for function '{$function}'."); + throw new RuntimeException("ORM Query: Not enough arguments for function '{$function}'."); } $operatorArgumentList = $argumentPartList; array_shift($operatorArgumentList); @@ -888,7 +893,7 @@ abstract class BaseQuery protected function getFunctionPartTZ(string $entityType, ?array $argumentPartList = null) { if (!$argumentPartList || count($argumentPartList) < 2) { - throw new Error("ORM Query: Not enough arguments for function TZ."); + throw new RuntimeException("ORM Query: Not enough arguments for function TZ."); } $offsetHoursString = $argumentPartList[1]; if (substr($offsetHoursString, 0, 1) === '\'' && substr($offsetHoursString, -1) === '\'') { @@ -914,14 +919,14 @@ abstract class BaseQuery { $delimiterPosition = strpos($expression, ':'); if ($delimiterPosition === false) { - throw new Error("ORM Query: Bad MATCH usage."); + throw new RuntimeException("ORM Query: Bad MATCH usage."); } $function = substr($expression, 0, $delimiterPosition); $rest = substr($expression, $delimiterPosition + 1); if (empty($rest)) { - throw new Error("ORM Query: Empty MATCH parameters."); + throw new RuntimeException("ORM Query: Empty MATCH parameters."); } if (substr($rest, 0, 1) === '(' && substr($rest, -1) === ')') { @@ -929,7 +934,7 @@ abstract class BaseQuery $argumentList = self::parseArgumentListFromFunctionContent($rest); if (count($argumentList) < 2) { - throw new Error("ORM Query: Bad MATCH usage."); + throw new RuntimeException("ORM Query: Bad MATCH usage."); } $columnList = []; @@ -940,7 +945,7 @@ abstract class BaseQuery } else { $delimiterPosition = strpos($rest, ':'); if ($delimiterPosition === false) { - throw new Error("ORM Query: Bad MATCH usage."); + throw new RuntimeException("ORM Query: Bad MATCH usage."); } $columns = substr($rest, 0, $delimiterPosition); @@ -1392,7 +1397,7 @@ abstract class BaseQuery $r = $entity->relations[$relationName]; } - $keySet = $this->getKeys($entity, $relationName); + $keySet = $this->helper->getRelationKeys($entity, $relationName); $key = $keySet['key']; $foreignKey = $keySet['foreignKey']; @@ -1598,6 +1603,9 @@ abstract class BaseQuery } } + /** + * Convert a camelCase string to a corresponding representation for DB. + */ public function toDb(string $attribute) : string { if (!array_key_exists($attribute, $this->attributeDbMapCache)) { @@ -1717,16 +1725,6 @@ abstract class BaseQuery return $fieldPath; } - /** - * Compose a WHERE query part from a given where clause. - */ - public function buildWherePart(string $entityType, array $whereClause) : string - { - $entity = $this->getSeed($entityType); - - return $this->getWherePart($entity, $whereClause); - } - protected function getWherePart( Entity $entity, ?array $whereClause = null, string $sqlOp = 'AND', array &$params = [], int $level = 0 ) : string { @@ -1996,6 +1994,9 @@ abstract class BaseQuery return $joinAlias; } + /** + * Convert a value to a string. + */ public function stringifyValue($value) : string { if (is_array($value)) { @@ -2011,11 +2012,17 @@ abstract class BaseQuery return $stringValue; } + /** + * Sanitize a string. + */ public function sanitize(string $string) : string { return preg_replace('/[^A-Za-z0-9_]+/', '', $string); } + /** + * Sanitize an alias for a SELECT statement. + */ public function sanitizeSelectAlias(string $string) : string { $string = preg_replace('/[^A-Za-z\r\n0-9_:\'" .,\-\(\)]+/', '', $string); @@ -2196,7 +2203,7 @@ abstract class BaseQuery $relationName = $name; $relParams = $entity->getRelations()[$relationName]; - $keySet = $this->getKeys($entity, $relationName); + $keySet = $this->helper->getRelationKeys($entity, $relationName); if (!$alias) { $alias = $relationName; @@ -2327,7 +2334,7 @@ abstract class BaseQuery return false; } - public function composeSelectQuery( + protected function composeSelectQuery( string $table, string $select, ?string $joins = null, @@ -2462,7 +2469,7 @@ abstract class BaseQuery protected function getSetPart(Entity $entity, array $values) : string { if (!count($values)) { - throw new Error("ORM Query: No SET values for update query."); + throw new RuntimeException("ORM Query: No SET values for update query."); } $list = []; @@ -2535,95 +2542,4 @@ abstract class BaseQuery * Add a LIMIT part to a SQL query. */ abstract public function limit(string $sql, ?int $offset = null, ?int $limit = null) : string; - - /** - * Get relation keys. - */ - public function getKeys(Entity $entity, string $relationName) : array - { - $relParams = $entity->getRelations()[$relationName]; - $relType = $relParams['type']; - - switch ($relType) { - case Entity::BELONGS_TO: - $key = $this->toDb($entity->getEntityType()) . 'Id'; - if (isset($relParams['key'])) { - $key = $relParams['key']; - } - $foreignKey = 'id'; - if (isset($relParams['foreignKey'])){ - $foreignKey = $relParams['foreignKey']; - } - return [ - 'key' => $key, - 'foreignKey' => $foreignKey, - ]; - - case Entity::HAS_MANY: - case Entity::HAS_ONE: - $key = 'id'; - if (isset($relParams['key'])){ - $key = $relParams['key']; - } - $foreignKey = $this->toDb($entity->getEntityType()) . 'Id'; - if (isset($relParams['foreignKey'])) { - $foreignKey = $relParams['foreignKey']; - } - return [ - 'key' => $key, - 'foreignKey' => $foreignKey, - ]; - - case Entity::HAS_CHILDREN: - $key = 'id'; - if (isset($relParams['key'])){ - $key = $relParams['key']; - } - $foreignKey = 'parentId'; - if (isset($relParams['foreignKey'])) { - $foreignKey = $relParams['foreignKey']; - } - $foreignType = 'parentType'; - if (isset($relParams['foreignType'])) { - $foreignType = $relParams['foreignType']; - } - return [ - 'key' => $key, - 'foreignKey' => $foreignKey, - 'foreignType' => $foreignType, - ]; - - case Entity::MANY_MANY: - $key = 'id'; - if(isset($relParams['key'])){ - $key = $relParams['key']; - } - $foreignKey = 'id'; - if(isset($relParams['foreignKey'])){ - $foreignKey = $relParams['foreignKey']; - } - $nearKey = $this->toDb($entity->getEntityType()) . 'Id'; - $distantKey = $this->toDb($relParams['entity']) . 'Id'; - if (isset($relParams['midKeys']) && is_array($relParams['midKeys'])){ - $nearKey = $relParams['midKeys'][0]; - $distantKey = $relParams['midKeys'][1]; - } - return [ - 'key' => $key, - 'foreignKey' => $foreignKey, - 'nearKey' => $nearKey, - 'distantKey' => $distantKey - ]; - case Entity::BELONGS_TO_PARENT: - $key = $relationName . 'Id'; - $typeKey = $relationName . 'Type'; - return [ - 'key' => $key, - 'typeKey' => $typeKey, - 'foreignKey' => 'id', - ]; - } - - return null; - } } diff --git a/application/Espo/ORM/EntityManager.php b/application/Espo/ORM/EntityManager.php index 17bf19850c..8ada105554 100644 --- a/application/Espo/ORM/EntityManager.php +++ b/application/Espo/ORM/EntityManager.php @@ -96,6 +96,8 @@ class EntityManager $this->repositoryFactory = $repositoryFactory; $this->queryExecutor = new RDBQueryExecutor($this); + + $this->metadataHelper = new MetadataHelper($this->metadata); } /**