diff --git a/application/Espo/ORM/DB/BaseMapper.php b/application/Espo/ORM/DB/BaseMapper.php index 23cf06035b..1e6745d9d5 100644 --- a/application/Espo/ORM/DB/BaseMapper.php +++ b/application/Espo/ORM/DB/BaseMapper.php @@ -342,14 +342,10 @@ abstract class BaseMapper implements Mapper $additionalColumnsConditions = $params['additionalColumnsConditions']; } - $MMJoinPart = $this->getMMJoin($entity, $relationName, $keySet, $additionalColumnsConditions); + $params['joins'] = $params['joins'] ?? []; + + $params['joins'][] = $this->getManyManyJoin($entity, $relationName, $additionalColumnsConditions); - if (empty($params['customJoin'])) { - $params['customJoin'] = ''; - } else { - $params['customJoin'] .= ' '; - } - $params['customJoin'] .= $MMJoinPart; $params['relationName'] = $relDefs['relationName']; @@ -1274,38 +1270,41 @@ abstract class BaseMapper implements Mapper return $entity; } - protected function getMMJoin(Entity $entity, string $relationName, $keySet = false, $conditions = []) : string + protected function getManyManyJoin(Entity $entity, string $relationName, ?array $conditions = null) : array { - $relDefs = $entity->relations[$relationName]; + $defs = $entity->relations[$relationName]; - if (empty($keySet)) { - $keySet = $this->query->getKeys($entity, $relationName); - } + $middleName = $defs['relationName'] ?? null; + + $keySet = $this->query->getKeys($entity, $relationName); $key = $keySet['key']; $foreignKey = $keySet['foreignKey']; $nearKey = $keySet['nearKey']; $distantKey = $keySet['distantKey']; - $relTable = $this->toDb($relDefs['relationName']); - $distantTable = $this->toDb($relDefs['entity']); + if (!$middleName) { + throw new Error("No 'relationName' parameter for '{$relationName}' relationship."); + } - $join = - "JOIN `{$relTable}` ON {$distantTable}." . $this->toDb($foreignKey) . " = {$relTable}." . $this->toDb($distantKey) - . " AND " - . "{$relTable}." . $this->toDb($nearKey) . " = " . $this->pdo->quote($entity->get($key)) - . " AND " - . "{$relTable}.deleted = " . $this->quote(false) . ""; + $alias = lcfirst($middleName); + + $join = [ + ucfirst($middleName), + $alias, + [ + "{$distantKey}:" => $foreignKey, + "{$nearKey}" => $entity->get($key), + "deleted" => false, + ], + ]; $conditions = $conditions ?? []; - if (!empty($relDefs['conditions']) && is_array($relDefs['conditions'])) { - $conditions = array_merge($conditions, $relDefs['conditions']); + if (!empty($defs['conditions']) && is_array($defs['conditions'])) { + $conditions = array_merge($conditions, $defs['conditions']); } - if (!empty($conditions)) { - $conditionsSql = $this->query->buildJoinConditionsStatement($entity->getEntityType(), $relTable, $conditions); - $join .= " AND " . $conditionsSql; - } + $join[2] = array_merge($join[2], $conditions); return $join; } diff --git a/application/Espo/ORM/DB/Query/BaseQuery.php b/application/Espo/ORM/DB/Query/BaseQuery.php index 9adb33d4d2..1b9e070f69 100644 --- a/application/Espo/ORM/DB/Query/BaseQuery.php +++ b/application/Espo/ORM/DB/Query/BaseQuery.php @@ -684,13 +684,13 @@ abstract class BaseQuery } if ( - !empty($params['additionalColumns']) && is_array($params['additionalColumns']) && - !empty($params['relationName']) + !empty($params['additionalColumns']) && is_array($params['additionalColumns']) && !empty($params['relationName']) ) { + $alias = $this->sanitizeSelectAlias(lcfirst($params['relationName'])); + foreach ($params['additionalColumns'] as $column => $field) { $itemAlias = $this->sanitizeSelectAlias($field); - $selectPart .= ", `" . $this->toDb($this->sanitize($params['relationName'])) . "`." . - $this->toDb($this->sanitize($column)) . " AS `{$itemAlias}`"; + $selectPart .= ", " . $alias . "." . $this->toDb($this->sanitize($column)) . " AS `{$itemAlias}`"; } } @@ -2035,23 +2035,6 @@ abstract class BaseQuery return implode(' ', $joinSqlList); } - public function buildJoinConditionsStatement(string $entityType, string $alias, array $conditions) : string - { - $entity = $this->getSeed($entityType); - - $sql = ''; - - $joinSqlList = []; - foreach ($conditions as $left => $right) { - $joinSqlList[] = $this->buildJoinConditionStatement($entity, $alias, $left, $right); - } - if (count($joinSqlList)) { - $sql .= implode(" AND ", $joinSqlList); - } - - return $sql; - } - protected function buildJoinConditionStatement(Entity $entity, $alias = null, $left, $right) { $sql = ''; diff --git a/tests/unit/Espo/ORM/DB/MapperTest.php b/tests/unit/Espo/ORM/DB/MapperTest.php index b1f9e56e98..d537f05702 100644 --- a/tests/unit/Espo/ORM/DB/MapperTest.php +++ b/tests/unit/Espo/ORM/DB/MapperTest.php @@ -248,15 +248,15 @@ class DBMapperTest extends \PHPUnit\Framework\TestCase $query = "SELECT tag.id AS `id`, tag.name AS `name`, tag.deleted AS `deleted` ". "FROM `tag` ". - "JOIN `post_tag` ON tag.id = post_tag.tag_id AND post_tag.post_id = '1' AND post_tag.deleted = 0 ". + "JOIN `post_tag` AS `postTag` ON postTag.tag_id = tag.id AND postTag.post_id = '1' AND postTag.deleted = 0 ". "WHERE tag.deleted = 0"; - $return = new MockDBResult(array( - array( + $return = new MockDBResult([ + [ 'id' => '1', 'name' => 'test', 'deleted' => false, - ), - )); + ], + ]); $this->mockQuery($query, $return); $this->post->id = '1'; $list = $this->db->selectRelated($this->post, 'tags'); @@ -266,10 +266,34 @@ class DBMapperTest extends \PHPUnit\Framework\TestCase $this->assertEquals($list[0]->get('name'), 'test'); } + public function testSelectRelatedManyManyWithConditions() + { + $query = + "SELECT team.id AS `id`, team.name AS `name`, team.deleted AS `deleted`, entityTeam.team_id AS `stub` FROM `team` ". + "JOIN `entity_team` AS `entityTeam` ON entityTeam.team_id = team.id AND entityTeam.entity_id = '1' AND " . + "entityTeam.deleted = 0 AND entityTeam.entity_type = 'Account' WHERE team.deleted = 0"; + $return = new MockDBResult([ + [ + 'id' => '1', + 'name' => 'test', + 'deleted' => false, + ], + ]); + $this->mockQuery($query, $return); + $this->account->id = '1'; + + $list = $this->db->selectRelated($this->account, 'teams', [ + 'additionalColumns' => [ + 'teamId' => 'stub', + ], + ]); + } + public function testSelectRelatedHasChildren() { $query = - "SELECT note.id AS `id`, note.name AS `name`, note.parent_id AS `parentId`, note.parent_type AS `parentType`, note.deleted AS `deleted` ". + "SELECT ". + "note.id AS `id`, note.name AS `name`, note.parent_id AS `parentId`, note.parent_type AS `parentType`, note.deleted AS `deleted` ". "FROM `note` ". "WHERE note.parent_id = '1' AND note.parent_type = 'Post' AND note.deleted = 0"; $return = new MockDBResult(array( @@ -291,7 +315,10 @@ class DBMapperTest extends \PHPUnit\Framework\TestCase public function testSelectRelatedBelongsTo() { $query = - "SELECT post.id AS `id`, post.name AS `name`, NULLIF(TRIM(CONCAT(IFNULL(createdBy.salutation_name, ''), IFNULL(createdBy.first_name, ''), ' ', IFNULL(createdBy.last_name, ''))), '') AS `createdByName`, post.created_by_id AS `createdById`, post.deleted AS `deleted` ". + "SELECT ". + "post.id AS `id`, post.name AS `name`, NULLIF(TRIM(CONCAT(IFNULL(createdBy.salutation_name, ''), ". + "IFNULL(createdBy.first_name, ''), ' ', IFNULL(createdBy.last_name, ''))), '') AS `createdByName`, ". + "post.created_by_id AS `createdById`, post.deleted AS `deleted` ". "FROM `post` ". "LEFT JOIN `user` AS `createdBy` ON post.created_by_id = createdBy.id " . "WHERE post.id = '1' AND post.deleted = 0 ". @@ -320,7 +347,7 @@ class DBMapperTest extends \PHPUnit\Framework\TestCase $query = "SELECT COUNT(tag.id) AS `value` ". "FROM `tag` ". - "JOIN `post_tag` ON tag.id = post_tag.tag_id AND post_tag.post_id = '1' AND post_tag.deleted = 0 ". + "JOIN `post_tag` AS `postTag` ON postTag.tag_id = tag.id AND postTag.post_id = '1' AND postTag.deleted = 0 ". "WHERE tag.deleted = 0"; $return = new MockDBResult([ [ diff --git a/tests/unit/testData/DB/Entities.php b/tests/unit/testData/DB/Entities.php index 30cb1db7b0..d59afc6111 100644 --- a/tests/unit/testData/DB/Entities.php +++ b/tests/unit/testData/DB/Entities.php @@ -24,6 +24,10 @@ class Account extends TEntity 'type' => Entity::BOOL, 'default' => 0, ), + 'stub' => [ + 'type' => 'varchar', + 'notStorable' => true, + ], ); public $relations = array( 'teams' => array(