Skip to content

Improve perfomance #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
02752c1
Improve perfomance
arogachev Sep 20, 2023
76a0b99
Update CHANGELOG [skip ci]
arogachev Sep 20, 2023
a56d91d
An attempt to fix MySQL 5.7
arogachev Sep 22, 2023
2ff5fb6
An attempt to fix Oracle
arogachev Sep 22, 2023
bf3ab2b
An attempt to fix Oracle 2
arogachev Sep 22, 2023
f67cabf
Apply fixes from StyleCI
StyleCIBot Sep 22, 2023
3870a94
Update src/ItemTreeTraversal/MysqlItemTreeTraversal.php
arogachev Sep 25, 2023
5b33f41
Optimization - specifying select fields [skip ci]
arogachev Sep 25, 2023
1df2b5e
Update src/ItemTreeTraversal/MysqlItemTreeTraversal.php
arogachev Sep 25, 2023
36cd559
Revert "Update src/ItemTreeTraversal/MysqlItemTreeTraversal.php" [ski…
arogachev Sep 25, 2023
35a72ef
Update src/ItemTreeTraversal/MysqlItemTreeTraversal.php
arogachev Sep 25, 2023
fb92c64
Test suggested from option
arogachev Sep 25, 2023
0fe73ec
Merge remote-tracking branch 'origin/improve-perfomance' into improve…
arogachev Sep 25, 2023
344bc2b
Test suggested from option
arogachev Sep 25, 2023
38de496
Fix Psalm [skip ci]
arogachev Sep 26, 2023
c2bd6a5
Apply suggestion for simplifying traversal query
arogachev Sep 26, 2023
9de7184
Remove dead code [skip ci]
arogachev Sep 26, 2023
bdf10e6
Try to fix MSSQL and Oracle differently
arogachev Sep 26, 2023
eb55644
Update CHANGELOG [skip ci]
arogachev Sep 26, 2023
00395e6
More optimization by Tigrov
arogachev Sep 26, 2023
6db6f55
Fix typo in CHANGELOG [skip ci]
arogachev Sep 26, 2023
3155ba4
Fix copy paste in CHANGELOG [skip ci]
arogachev Sep 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@
- Enh #26: Add default table names (@arogachev)
- Chg #25: Use prefix for default table names (@arogachev)
- Bug #44: Fix hardcoded items children table name in item tree traversal query for MySQL 5 (@arogachev)
- Enh #46: Improve performance (@arogachev, @Tigrov)
- Enh #46: Rename `getChildren()` method to `getDirectAchildren()` in `ItemsStorage` (@arogachev)
- Enh #46: Add methods to `ItemsStorage`:
- `roleExists()`;
- `getRolesByNames()`;
- `getPermissionsByNames()`;
- `getAllChildren()`;
- `getAllChildRoles()`;
- `getAllChildPermissions()`;
- `hasChild()`;
- `hasDirectChild()`.
(@arogachev)
- Enh #46: Add methods to `AssignmentsStorage`:
- `getByItemNames()`;
- `exists()`;
- `userHasItem()`.
(@arogachev)

## 1.0.0 April 20, 2023

Expand Down
73 changes: 60 additions & 13 deletions src/AssignmentsStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,42 +55,89 @@ public function getAll(): array

public function getByUserId(string $userId): array
{
/** @psalm-var RawAssignment[] $rows */
$rows = (new Query($this->database))
/** @psalm-var list<array{itemName: string, createdAt: int|string}> $rawAssignments */
$rawAssignments = (new Query($this->database))
->select(['itemName', 'createdAt'])
->from($this->tableName)
->where(['userId' => $userId])
->all();
$assignments = [];
foreach ($rawAssignments as $rawAssignment) {
$assignments[$rawAssignment['itemName']] = new Assignment(
$userId,
$rawAssignment['itemName'],
(int) $rawAssignment['createdAt'],
);
}

return array_combine(
array_column($rows, 'itemName'),
array_map(
static fn(array $row): Assignment => new Assignment($userId, $row['itemName'], (int) $row['createdAt']),
$rows,
)
);
return $assignments;
}

public function getByItemNames(array $itemNames): array
{
if (empty($itemNames)) {
return [];
}

/** @psalm-var RawAssignment[] $rawAssignments */
$rawAssignments = (new Query($this->database))
->from($this->tableName)
->where(['itemName' => $itemNames])
->all();
$assignments = [];
foreach ($rawAssignments as $rawAssignment) {
$assignments[] = new Assignment(
$rawAssignment['userId'],
$rawAssignment['itemName'],
(int) $rawAssignment['createdAt'],
);
}

return $assignments;
}

public function get(string $itemName, string $userId): ?Assignment
{
/** @psalm-var RawAssignment|null $row */
$row = (new Query($this->database))
->select(['createdAt'])
->from($this->tableName)
->where(['itemName' => $itemName, 'userId' => $userId])
->one();

return $row === null ? null : new Assignment($row['userId'], $row['itemName'], (int) $row['createdAt']);
return $row === null ? null : new Assignment($userId, $itemName, (int) $row['createdAt']);
}

public function exists(string $itemName, string $userId): bool
{
return (new Query($this->database))
->from($this->tableName)
->where(['itemName' => $itemName, 'userId' => $userId])
->exists();
}

public function userHasItem(string $userId, array $itemNames): bool
{
if (empty($itemNames)) {
return false;
}

return (new Query($this->database))
->from($this->tableName)
->where(['userId' => $userId, 'itemName' => $itemNames])
->exists();
}

public function add(string $itemName, string $userId): void
public function add(Assignment $assignment): void
{
$this
->database
->createCommand()
->insert(
$this->tableName,
[
'itemName' => $itemName,
'userId' => $userId,
'itemName' => $assignment->getItemName(),
'userId' => $assignment->getUserId(),
'createdAt' => time(),
],
)
Expand Down
129 changes: 93 additions & 36 deletions src/ItemTreeTraversal/CteItemTreeTraversal.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@

namespace Yiisoft\Rbac\Db\ItemTreeTraversal;

use Yiisoft\Db\Command\CommandInterface;
use Yiisoft\Db\Connection\ConnectionInterface;
use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Query\Query;
use Yiisoft\Db\Query\QueryInterface;
use Yiisoft\Rbac\Db\ItemsStorage;
use Yiisoft\Rbac\Item;

/**
* A RBAC item tree traversal strategy based on CTE (common table expression). Uses `WITH` expression to form a
Expand All @@ -18,6 +23,8 @@
*/
abstract class CteItemTreeTraversal implements ItemTreeTraversalInterface
{
protected bool $useRecursiveInWith = true;

/**
* @param ConnectionInterface $database Yii Database connection instance.
*
Expand All @@ -36,52 +43,102 @@ public function __construct(

public function getParentRows(string $name): array
{
$sql = "{$this->getWithExpression()} parent_of(child_name) AS (
SELECT [[name]] FROM {{%$this->tableName}} WHERE [[name]] = :name_for_recursion
UNION ALL
SELECT [[parent]] FROM {{%$this->childrenTableName}} item_child_recursive, parent_of
WHERE item_child_recursive.[[child]] = parent_of.child_name
)
SELECT {{%item}}.* FROM parent_of
LEFT JOIN {{%$this->tableName}} {{%item}} ON {{%item}}.[[name]] = parent_of.child_name
WHERE {{%item}}.[[name]] != :excluded_name";
$baseOuterQuery = (new Query($this->database))->select('item.*')->where(['!=', 'item.name', $name]);

/** @psalm-var RawItem[] */
return $this
->database
->createCommand($sql, [':name_for_recursion' => $name, ':excluded_name' => $name])
->queryAll();
return $this->getRowsCommand($name, baseOuterQuery: $baseOuterQuery)->queryAll();
}

public function getChildrenRows(string $name): array
{
$sql = "{$this->getWithExpression()} child_of(parent_name) AS (
SELECT [[name]] FROM {{%$this->tableName}} WHERE [[name]] = :name_for_recursion
UNION ALL
SELECT [[child]] FROM {{%$this->childrenTableName}} item_child_recursive, child_of
WHERE item_child_recursive.[[parent]] = child_of.parent_name
)
SELECT {{%item}}.* FROM child_of
LEFT JOIN {{%$this->tableName}} {{%item}} ON {{%item}}.[[name]] = child_of.parent_name
WHERE {{%item}}.[[name]] != :excluded_name";
$baseOuterQuery = (new Query($this->database))->select('item.*')->where(['!=', 'item.name', $name]);

/** @psalm-var RawItem[] */
return $this
->database
->createCommand($sql, [':name_for_recursion' => $name, ':excluded_name' => $name])
->queryAll();
return $this->getRowsCommand($name, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll();
}

/**
* Gets `WITH` expression used in DB query.
*
* @infection-ignore-all
* - ProtectedVisibility.
*
* @return string `WITH` expression.
*/
protected function getWithExpression(): string
public function getChildPermissionRows(string $name): array
{
$baseOuterQuery = (new Query($this->database))
->select('item.*')
->where(['!=', 'item.name', $name])
->andWhere(['item.type' => Item::TYPE_PERMISSION]);

/** @psalm-var RawItem[] */
return $this->getRowsCommand($name, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll();
}

public function getChildRoleRows(string $name): array
{
return 'WITH RECURSIVE';
$baseOuterQuery = (new Query($this->database))
->select('item.*')
->where(['!=', 'item.name', $name])
->andWhere(['item.type' => Item::TYPE_ROLE]);

/** @psalm-var RawItem[] */
return $this->getRowsCommand($name, baseOuterQuery: $baseOuterQuery, areParents: false)->queryAll();
}

public function hasChild(string $parentName, string $childName): bool
{
/**
* @infection-ignore-all
* - ArrayItemRemoval, select.
*/
$baseOuterQuery = (new Query($this->database))
->select([new Expression('1 AS item_child_exists')])
->andWhere(['item.name' => $childName]);
/** @psalm-var array<0, 1>|false $result */
$result = $this
->getRowsCommand($parentName, baseOuterQuery: $baseOuterQuery, areParents: false)
->queryScalar();

return $result !== false;
}

private function getRowsCommand(
string $name,
QueryInterface $baseOuterQuery,
bool $areParents = true,
): CommandInterface {
if ($areParents) {
$cteSelectRelationName = 'parent';
$cteConditionRelationName = 'child';
$cteName = 'parent_of';
$cteParameterName = 'child_name';
} else {
$cteSelectRelationName = 'child';
$cteConditionRelationName = 'parent';
$cteName = 'child_of';
$cteParameterName = 'parent_name';
}

$cteSelectRelationQuery = (new Query($this->database))
->select($cteSelectRelationName)
->from(['item_child_recursive' => $this->childrenTableName])
->innerJoin($cteName, [
"item_child_recursive.$cteConditionRelationName" => new Expression(
"{{{$cteName}}}.[[$cteParameterName]]",
),
]);
$cteSelectItemQuery = (new Query($this->database))
->select('name')
->from($this->tableName)
->where(['name' => $name])
->union($cteSelectRelationQuery, all: true);
$quoter = $this->database->getQuoter();
$outerQuery = $baseOuterQuery
->withQuery(
$cteSelectItemQuery,
$quoter->quoteTableName($cteName) . '(' . $quoter->quoteColumnName($cteParameterName) . ')',
recursive: $this->useRecursiveInWith,
)
->from($cteName)
->leftJoin(
['item' => $this->tableName],
['item.name' => new Expression("{{{$cteName}}}.[[$cteParameterName]]")],
);

return $outerQuery->createCommand();
}
}
30 changes: 30 additions & 0 deletions src/ItemTreeTraversal/ItemTreeTraversalInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,34 @@ public function getParentRows(string $name): array;
* @psalm-return RawItem[]
*/
public function getChildrenRows(string $name): array;

/**
* Get all child permission rows for an item by the given name.
*
* @param string $name Item name.
*
* @return array Flat list of all child permissions.
* @psalm-return RawItem[]
*/
public function getChildPermissionRows(string $name): array;

/**
* Get all child role rows for an item by the given name.
*
* @param string $name Item name.
*
* @return array Flat list of all child roles.
* @psalm-return RawItem[]
*/
public function getChildRoleRows(string $name): array;

/**
* Whether a selected parent has specific child.
*
* @param string $parentName Parent item name.
* @param string $childName Child item name.
*
* @return bool Whether a selected parent has specific child.
*/
public function hasChild(string $parentName, string $childName): bool;
}
5 changes: 1 addition & 4 deletions src/ItemTreeTraversal/MssqlCteItemTreeTraversal.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,5 @@
*/
final class MssqlCteItemTreeTraversal extends CteItemTreeTraversal
{
public function getWithExpression(): string
{
return 'WITH';
}
protected bool $useRecursiveInWith = false;
}
Loading