Skip to content

Commit

Permalink
Merge pull request #5000 from pmattmann/feature/perf-showcase
Browse files Browse the repository at this point in the history
Database Query Performance
  • Loading branch information
usu committed Apr 28, 2024
2 parents 230c1c1 + d5b4c12 commit 95b1ba3
Show file tree
Hide file tree
Showing 22 changed files with 160 additions and 94 deletions.
1 change: 1 addition & 0 deletions api/config/packages/doctrine.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ doctrine:
schema_ignore_classes:
- App\Entity\CampRootContentNode
- App\Entity\PeriodMaterialItem
- App\Entity\UserCamp
36 changes: 36 additions & 0 deletions api/migrations/schema/Version20240414092957.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20240414092957 extends AbstractMigration {
public function getDescription(): string {
return 'Add View view_user_camps to fast resolve visible camps';
}

public function up(Schema $schema): void {
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(
<<<'EOF'
CREATE OR REPLACE VIEW public.view_user_camps
AS
SELECT CONCAT(u.id, c.id) id, u.id userid, c.id campid
from camp c, "user" u
where c.isprototype = TRUE
union all
select cc.id, cc.userid, cc.campid
from camp_collaboration cc
where cc.status = 'established'
EOF
);
}

public function down(Schema $schema): void {
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP VIEW public.view_user_camps');
}
}
11 changes: 4 additions & 7 deletions api/src/Doctrine/Filter/ContentNodePeriodFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
use App\Doctrine\QueryBuilderHelper;
use App\Entity\Activity;
use App\Entity\ContentNode;
use App\Repository\FiltersByCampCollaboration;
Expand Down Expand Up @@ -66,21 +65,19 @@ protected function filterProperty(

// generate alias to avoid interference with other filters
$periodParameterName = $queryNameGenerator->generateParameterName($property);
$rootJoinAlias = $queryNameGenerator->generateJoinAlias('root');
$activityJoinAlias = $queryNameGenerator->generateJoinAlias('activity');
$scheduleEntryJoinAlias = $queryNameGenerator->generateJoinAlias('scheduleEntry');

$rootAlias = $queryBuilder->getRootAliases()[0];

$rootQry = $queryBuilder->getEntityManager()->createQueryBuilder();
$rootQry->from(ContentNode::class, $rootJoinAlias)
->select($rootJoinAlias)
->innerJoin(Activity::class, $activityJoinAlias, Join::WITH, "{$activityJoinAlias}.rootContentNode = {$rootJoinAlias}.id")
$rootQry
->select("identity({$activityJoinAlias}.rootContentNode)")
->from(Activity::class, $activityJoinAlias)
->innerJoin("{$activityJoinAlias}.scheduleEntries", $scheduleEntryJoinAlias, Join::WITH, $queryBuilder->expr()->eq("{$scheduleEntryJoinAlias}.period", ":{$periodParameterName}"))
;
$rootQry->setParameter($periodParameterName, $period);

$queryBuilder->andWhere($queryBuilder->expr()->in("{$rootAlias}.root", $rootQry->getDQL()));
QueryBuilderHelper::copyParameters($queryBuilder, $rootQry);
$queryBuilder->setParameter($periodParameterName, $period);
}
}
10 changes: 4 additions & 6 deletions api/src/Doctrine/Filter/MaterialItemPeriodFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
use App\Doctrine\QueryBuilderHelper;
use App\Entity\MaterialItem;
use App\Entity\PeriodMaterialItem;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -61,18 +61,16 @@ protected function filterProperty(

// generate alias to avoid interference with other filters
$periodParameterName = $queryNameGenerator->generateParameterName($property);
$materialItem = $queryNameGenerator->generateJoinAlias('materialItem');
$periodMaterialItems = $queryNameGenerator->generateJoinAlias('periodMaterialItem');

$rootAlias = $queryBuilder->getRootAliases()[0];

$materialItemQry = $queryBuilder->getEntityManager()->createQueryBuilder();
$materialItemQry->from(MaterialItem::class, $materialItem)->select($materialItem);
$materialItemQry->join("{$materialItem}.periodMaterialItems", $periodMaterialItems);
$materialItemQry->select("identity({$periodMaterialItems}.materialItem)");
$materialItemQry->from(PeriodMaterialItem::class, $periodMaterialItems);
$materialItemQry->where($queryBuilder->expr()->eq("{$periodMaterialItems}.period", ":{$periodParameterName}"));
$materialItemQry->setParameter($periodParameterName, $period);

$queryBuilder->andWhere($queryBuilder->expr()->in("{$rootAlias}", $materialItemQry->getDQL()));
QueryBuilderHelper::copyParameters($queryBuilder, $materialItemQry);
$queryBuilder->setParameter($periodParameterName, $period);
}
}
18 changes: 0 additions & 18 deletions api/src/Doctrine/QueryBuilderHelper.php

This file was deleted.

8 changes: 8 additions & 0 deletions api/src/Entity/Camp.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ class Camp extends BaseEntity implements BelongsToCampInterface, CopyFromPrototy
#[ORM\OneToMany(targetEntity: CampCollaboration::class, mappedBy: 'camp', orphanRemoval: true)]
public Collection $collaborations;

/**
* UserCamp Collections
* Based von view_user_camps; lists all user who can see this camp.
*/
#[ORM\OneToMany(targetEntity: UserCamp::class, mappedBy: 'camp')]
public Collection $userCamps;

/**
* The time periods of the camp, there must be at least one. Periods in a camp may not overlap.
* When creating a camp, the initial periods may be specified as nested payload, but updating,
Expand Down Expand Up @@ -361,6 +368,7 @@ class Camp extends BaseEntity implements BelongsToCampInterface, CopyFromPrototy
public function __construct() {
parent::__construct();
$this->collaborations = new ArrayCollection();
$this->userCamps = new ArrayCollection();
$this->periods = new ArrayCollection();
$this->categories = new ArrayCollection();
$this->progressLabels = new ArrayCollection();
Expand Down
8 changes: 8 additions & 0 deletions api/src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ class User extends BaseEntity implements UserInterface, PasswordAuthenticatedUse
#[ORM\OneToMany(targetEntity: CampCollaboration::class, mappedBy: 'user', orphanRemoval: true)]
public Collection $collaborations;

/**
* UserCamp Collections
* Based von view_user_camps; lists all camps a user can see.
*/
#[ORM\OneToMany(targetEntity: UserCamp::class, mappedBy: 'user')]
public Collection $userCamps;

/**
* The state of this user.
*/
Expand Down Expand Up @@ -163,6 +170,7 @@ public function __construct() {
parent::__construct();
$this->ownedCamps = new ArrayCollection();
$this->collaborations = new ArrayCollection();
$this->userCamps = new ArrayCollection();
}

/**
Expand Down
23 changes: 23 additions & 0 deletions api/src/Entity/UserCamp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* view_user_camps
* List all visible camps for each user.
*/
#[ORM\Entity(readOnly: true)]
#[ORM\Table(name: 'view_user_camps')]
class UserCamp {
#[ORM\Id]
#[ORM\Column(type: 'string', length: 32, nullable: false)]
public string $id;

#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'userCamps')]
public User $user;

#[ORM\ManyToOne(targetEntity: Camp::class, inversedBy: 'userCamps')]
public Camp $camp;
}
3 changes: 1 addition & 2 deletions api/src/Repository/ActivityProgressLabelRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ public function __construct(EntityManagerInterface $em, string $entityClass = Ac

public function filterByUser(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, User $user): void {
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->innerJoin("{$rootAlias}.camp", 'camp');
$this->filterByCampCollaboration($queryBuilder, $user);
$this->filterByCampCollaboration($queryBuilder, $user, "{$rootAlias}.camp");
}
}
3 changes: 1 addition & 2 deletions api/src/Repository/ActivityRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public function __construct(ManagerRegistry $registry) {

public function filterByUser(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, User $user): void {
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->innerJoin("{$rootAlias}.camp", 'camp');
$this->filterByCampCollaboration($queryBuilder, $user);
$this->filterByCampCollaboration($queryBuilder, $user, "{$rootAlias}.camp");
}
}
14 changes: 11 additions & 3 deletions api/src/Repository/ActivityResponsibleRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
namespace App\Repository;

use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\Activity;
use App\Entity\ActivityResponsible;
use App\Entity\User;
use App\Entity\UserCamp;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;

Expand All @@ -23,9 +26,14 @@ public function __construct(ManagerRegistry $registry) {
}

public function filterByUser(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, User $user): void {
$activityQry = $this->getEntityManager()->createQueryBuilder();
$activityQry->select('a');
$activityQry->from(Activity::class, 'a');
$activityQry->join(UserCamp::class, 'uc', Join::WITH, 'a.camp = uc.camp');
$activityQry->where('uc.user = :current_user');

$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->innerJoin("{$rootAlias}.activity", 'activity');
$queryBuilder->innerJoin('activity.camp', 'camp');
$this->filterByCampCollaboration($queryBuilder, $user);
$queryBuilder->andWhere($queryBuilder->expr()->in("{$rootAlias}.activity", $activityQry->getDQL()));
$queryBuilder->setParameter('current_user', $user);
}
}
3 changes: 1 addition & 2 deletions api/src/Repository/CampCollaborationRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ public function findAllByPersonallyInvitedUser(User $user): array {

public function filterByUser(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, User $user): void {
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->innerJoin("{$rootAlias}.camp", 'camp');
$this->filterByCampCollaboration($queryBuilder, $user);
$this->filterByCampCollaboration($queryBuilder, $user, "{$rootAlias}.camp");
}
}
3 changes: 1 addition & 2 deletions api/src/Repository/CategoryRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public function __construct(ManagerRegistry $registry) {

public function filterByUser(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, User $user): void {
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->innerJoin("{$rootAlias}.camp", 'camp');
$this->filterByCampCollaboration($queryBuilder, $user);
$this->filterByCampCollaboration($queryBuilder, $user, "{$rootAlias}.camp");
}
}
14 changes: 11 additions & 3 deletions api/src/Repository/DayRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\Day;
use App\Entity\Period;
use App\Entity\User;
use App\Entity\UserCamp;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;

Expand All @@ -23,9 +26,14 @@ public function __construct(ManagerRegistry $registry) {
}

public function filterByUser(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, User $user): void {
$periodQry = $this->getEntityManager()->createQueryBuilder();
$periodQry->select('p');
$periodQry->from(Period::class, 'p');
$periodQry->join(UserCamp::class, 'uc', Join::WITH, 'p.camp = uc.camp');
$periodQry->where('uc.user = :current_user');

$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->innerJoin("{$rootAlias}.period", 'period');
$queryBuilder->innerJoin('period.camp', 'camp');
$this->filterByCampCollaboration($queryBuilder, $user);
$queryBuilder->andWhere($queryBuilder->expr()->in("{$rootAlias}.period", $periodQry->getDQL()));
$queryBuilder->setParameter('current_user', $user);
}
}
21 changes: 10 additions & 11 deletions api/src/Repository/DayResponsibleRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
namespace App\Repository;

use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Doctrine\QueryBuilderHelper;
use App\Entity\Day;
use App\Entity\DayResponsible;
use App\Entity\User;
use App\Entity\UserCamp;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;

Expand All @@ -25,17 +26,15 @@ public function __construct(ManagerRegistry $registry) {
}

public function filterByUser(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, User $user): void {
$rootAlias = $queryBuilder->getRootAliases()[0];

$dayQry = $queryBuilder->getEntityManager()->createQueryBuilder();
$dayQry->from(Day::class, 'day')
->select('day')
->innerJoin('day.period', 'period')
->innerJoin('period.camp', 'camp')
;
$this->filterByCampCollaboration($dayQry, $user);
$dayQry = $this->getEntityManager()->createQueryBuilder();
$dayQry->select('d');
$dayQry->from(Day::class, 'd');
$dayQry->join('d.period', 'p');
$dayQry->join(UserCamp::class, 'uc', Join::WITH, 'p.camp = uc.camp');
$dayQry->where('uc.user = :current_user');

$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere($queryBuilder->expr()->in("{$rootAlias}.day", $dayQry->getDQL()));
QueryBuilderHelper::copyParameters($queryBuilder, $dayQry);
$queryBuilder->setParameter('current_user', $user);
}
}
28 changes: 7 additions & 21 deletions api/src/Repository/FiltersByCampCollaboration.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace App\Repository;

use App\Entity\CampCollaboration;
use App\Entity\Camp;
use App\Entity\User;
use Doctrine\ORM\Query\Expr\Join;
use App\Entity\UserCamp;
use Doctrine\ORM\QueryBuilder;

trait FiltersByCampCollaboration {
Expand All @@ -15,26 +15,12 @@ trait FiltersByCampCollaboration {
* the alias of the camp as the third argument if it's anything other than "camp".
*/
public function filterByCampCollaboration(QueryBuilder $queryBuilder, User $user, string $campAlias = 'camp'): void {
$queryBuilder->leftJoin(
"{$campAlias}.collaborations",
"filter_{$campAlias}_campCollaboration",
Join::WITH,
$queryBuilder->expr()->andX(
$queryBuilder->expr()->eq("filter_{$campAlias}_campCollaboration.user", ':current_user'),
$queryBuilder->expr()->eq("filter_{$campAlias}_campCollaboration.status", ':established'),
)
);
$queryBuilder->andWhere(
$queryBuilder->expr()->orX(
// user is established collaborator in the camp
$queryBuilder->expr()->isNotNull("filter_{$campAlias}_campCollaboration.id"),
$campsQry = $queryBuilder->getEntityManager()->createQueryBuilder();
$campsQry->select('identity(uc.camp)');
$campsQry->from(UserCamp::class, 'uc');
$campsQry->where('uc.user = :current_user');

// camp is a Prototype = all Prototypes are readable
$queryBuilder->expr()->eq("{$campAlias}.isPrototype", ':true'),
)
);
$queryBuilder->andWhere($queryBuilder->expr()->in($campAlias, $campsQry->getDQL()));
$queryBuilder->setParameter('current_user', $user);
$queryBuilder->setParameter('established', CampCollaboration::STATUS_ESTABLISHED);
$queryBuilder->setParameter('true', true);
}
}
Loading

0 comments on commit 95b1ba3

Please sign in to comment.