join only middle, filters change

This commit is contained in:
Yuri Kuznetsov
2023-02-11 23:18:06 +02:00
parent 16a313f659
commit 4658eb800e
5 changed files with 68 additions and 25 deletions
@@ -38,6 +38,8 @@ use Espo\Entities\ArrayValue;
use Espo\Entities\User;
use Espo\ORM\Defs as ORMDefs;
use Espo\ORM\Entity;
use Espo\ORM\Query\Part\Condition as Cond;
use Espo\ORM\Query\Part\Join;
use Espo\ORM\Query\Part\WhereClause;
use Espo\ORM\Query\Part\WhereItem as WhereClauseItem;
use Espo\ORM\Query\SelectBuilder as QueryBuilder;
@@ -1319,9 +1321,11 @@ class ItemGeneralConverter implements ItemConverter
if ($relationType == Entity::MANY_MANY) {
$key = $defs->getForeignMidKey();
$nearKey = $defs->getMidKey();
$middleEntityType = ucfirst($defs->getRelationshipName());
// IN-sub-query performs faster than EXISTS on MariaDB when multiple IDs.
// Left-join performs faster than Inner-join.
// Not joining a foreign table as it affects performance in MySQL.
$subQuery = QueryBuilder::create()
@@ -1385,20 +1389,26 @@ class ItemGeneralConverter implements ItemConverter
if ($relationType == Entity::MANY_MANY) {
$key = $defs->getForeignMidKey();
$nearKey = $defs->getMidKey();
$middleEntityType = ucfirst($defs->getRelationshipName());
$subQuery = QueryBuilder::create()
->select('id')
->from($this->entityType)
->leftJoin($middleEntityType, $alias, [
"{$alias}.{$nearKey}:" => 'id',
"{$alias}.deleted" => false,
])
->where(["{$alias}.{$key}=" => $value])
->build();
return ['id!=s' => $subQuery->getRaw()];
// MariaDB and MySQL performs slow, PostgreSQL fast.
return Cond::not(
Cond::exists(
QueryBuilder::create()
->from($this->entityType, 'sq')
->join(
Join::create($link, $alias)
->withOnlyMiddle()
)
->where(["{$alias}.{$key}" => $value])
->where(
Cond::equal(
Cond::column('sq.id'),
Cond::column(lcfirst($this->entityType) . '.id')
)
)
->build()
)
)->getRaw();
}
if (
@@ -1455,6 +1465,7 @@ class ItemGeneralConverter implements ItemConverter
$whereList = [];
foreach ($value as $targetId) {
// Only-middle join performs slower on MariaDB.
$sq = QueryBuilder::create()
->from($this->entityType)
->select('id')
+17
View File
@@ -41,6 +41,7 @@ class Join
private string $target;
private ?string $alias = null;
private ?WhereItem $conditions = null;
private bool $onlyMiddle = false;
private function __construct(string $target, ?string $alias = null)
{
@@ -87,6 +88,11 @@ class Join
return !$this->isTable();
}
public function isOnlyMiddle(): bool
{
return $this->onlyMiddle;
}
/**
* Create.
*
@@ -137,4 +143,15 @@ class Join
return $obj;
}
/**
* Join only middle table. For many-to-many relationships.
*/
public function withOnlyMiddle(bool $onlyMiddle = true): self
{
$obj = clone $this;
$obj->onlyMiddle = $onlyMiddle;
return $obj;
}
}
@@ -184,15 +184,18 @@ trait SelectingBuilderTrait
* @param Join|string $target
* A relation name or table. A relation name should be in camelCase, a table in CamelCase.
* @param string|null $alias An alias.
* @param WhereItem|array<mixed, mixed>|null $conditions Join conditions.
* @param WhereItem|array<string|int, mixed>|null $conditions Join conditions.
*
* @todo Support USE INDEX in Join.
*/
private function joinInternal(string $type, $target, ?string $alias = null, $conditions = null): self
{
$onlyMiddle = false;
if ($target instanceof Join) {
$alias = $alias ?? $target->getAlias();
$conditions = $conditions ?? $target->getConditions();
$onlyMiddle = $target->isOnlyMiddle();
$target = $target->getTarget();
}
@@ -229,6 +232,22 @@ trait SelectingBuilderTrait
return $this;
}
$params = [];
if ($noLeftAlias) {
$params['noLeftAlias'] = true;
}
if ($onlyMiddle) {
$params['onlyMiddle'] = true;
}
if ($params !== []) {
$this->params[$type][] = [$target, $alias, $conditions, $params];
return $this;
}
if (is_null($alias) && is_null($conditions)) {
$this->params[$type][] = $target;
@@ -241,13 +260,7 @@ trait SelectingBuilderTrait
return $this;
}
$item = [$target, $alias, $conditions];
if ($noLeftAlias) {
$item[] = ['noLeftAlias' => true];
}
$this->params[$type][] = $item;
$this->params[$type][] = [$target, $alias, $conditions];
return $this;
}
@@ -3274,7 +3274,11 @@ abstract class BaseQueryComposer implements QueryComposer
$distantTable = $this->toDb($foreignEntityType);
$midAlias = $alias . 'Middle';
$onlyMiddle = $joinParams['onlyMiddle'] ?? false;
$midAlias = $onlyMiddle ?
$alias :
$alias . 'Middle';
$indexKeyList = null;
$indexList = $joinParams['useIndex'] ?? null;
@@ -3339,8 +3343,6 @@ abstract class BaseQueryComposer implements QueryComposer
$sql .= " AND " . implode(" AND ", $joinSqlList);
}
$onlyMiddle = $joinParams['onlyMiddle'] ?? false;
if (!$onlyMiddle) {
$rightKeyColumn = $this->quoteColumn("{$alias}." . $this->toDb($foreignKey));
$middleDistantKeyColumn = $this->quoteColumn("{$midAlias}." . $this->toDb($distantKey));
@@ -954,7 +954,7 @@ class MysqlQueryComposerTest extends \PHPUnit\Framework\TestCase
$expectedSql =
"SELECT post.id AS `id` FROM `post` " .
"LEFT JOIN `post_tag` AS `tagsMiddle` ON post.id = tagsMiddle.post_id AND tagsMiddle.deleted = 0 " .
"LEFT JOIN `post_tag` AS `tags` ON post.id = tags.post_id AND tags.deleted = 0 " .
"WHERE post.deleted = 0";
$this->assertEquals($expectedSql, $sql);