Merge pull request #7165 from nextcloud/get-cards-at-once

perf(cards): fetch all cards at once
This commit is contained in:
Luka Trovic
2025-09-16 10:31:00 +02:00
committed by GitHub
5 changed files with 58 additions and 21 deletions

View File

@@ -131,7 +131,11 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $card; return $card;
} }
public function findAll($stackId, $limit = null, $offset = null, $since = -1) { /**
* @return Card[]
* @throws \OCP\DB\Exception
*/
public function findAll($stackId, ?int $limit = null, ?int $offset = null, int $since = -1) {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from('deck_cards') ->from('deck_cards')
@@ -146,6 +150,32 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb); return $this->findEntities($qb);
} }
/**
* @param int[] $stackIds
* @return array<int, null|Card[]>
* @throws \OCP\DB\Exception
*/
public function findAllForStacks(array $stackIds, ?int $limit = null, ?int $offset = null, int $since = -1): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_cards')
->where($qb->expr()->in('stack_id', $qb->createNamedParameter($stackIds, IQueryBuilder::PARAM_INT_ARRAY)))
->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT)))
->setMaxResults($limit)
->setFirstResult($offset)
->orderBy('order')
->addOrderBy('id');
$rawCards = $this->findEntities($qb);
$cards = array_fill_keys($stackIds, null);
foreach ($rawCards as $card) {
$cards[$card->getStackId()][] = $card;
}
return $cards;
}
public function queryCardsByBoard(int $boardId): IQueryBuilder { public function queryCardsByBoard(int $boardId): IQueryBuilder {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('c.*') $qb->select('c.*')

View File

@@ -15,6 +15,7 @@ use Sabre\VObject\Component\VCalendar;
* @method int getDeletedAt() * @method int getDeletedAt()
* @method int getLastModified() * @method int getLastModified()
* @method int getOrder() * @method int getOrder()
* @method Card[] getCards()
*/ */
class Stack extends RelationalEntity { class Stack extends RelationalEntity {
protected $title; protected $title;

View File

@@ -77,12 +77,10 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
/** /**
* @param numeric $boardId * @param numeric $boardId
* @param int|null $limit
* @param int|null $offset
* @return Stack[] * @return Stack[]
* @throws \OCP\DB\Exception * @throws \OCP\DB\Exception
*/ */
public function findAll($boardId, $limit = null, $offset = null): array { public function findAll($boardId, ?int $limit = null, ?int $offset = null): array {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from($this->getTableName()) ->from($this->getTableName())

View File

@@ -75,19 +75,22 @@ class StackService {
$this->stackServiceValidator = $stackServiceValidator; $this->stackServiceValidator = $stackServiceValidator;
} }
private function enrichStackWithCards($stack, $since = -1) { /** @param Stack[] $stacks */
$cards = $this->cardMapper->findAll($stack->getId(), null, null, $since); private function enrichStacksWithCards(array $stacks, $since = -1): void {
$cardsByStackId = $this->cardMapper->findAllForStacks(array_map(fn (Stack $stack) => $stack->getId(), $stacks), null, null, $since);
if (\count($cards) === 0) { foreach ($cardsByStackId as $stackId => $cards) {
return; if (!$cards) {
continue;
} }
$stack->setCards($this->cardService->enrichCards($cards));
}
private function enrichStacksWithCards($stacks, $since = -1) {
foreach ($stacks as $stack) { foreach ($stacks as $stack) {
$this->enrichStackWithCards($stack, $since); if ($stack->getId() === $stackId) {
$stack->setCards($this->cardService->enrichCards($cards));
break;
}
}
} }
} }
@@ -124,9 +127,9 @@ class StackService {
} }
/** /**
* @param $boardId * @param mixed $boardId
* *
* @return array * @return Stack[]
* @throws \OCA\Deck\NoPermissionException * @throws \OCA\Deck\NoPermissionException
* @throws BadRequestException * @throws BadRequestException
*/ */
@@ -247,7 +250,7 @@ class StackService {
); );
$this->changeHelper->boardChanged($stack->getBoardId()); $this->changeHelper->boardChanged($stack->getBoardId());
$this->eventDispatcher->dispatchTyped(new BoardUpdatedEvent($stack->getBoardId())); $this->eventDispatcher->dispatchTyped(new BoardUpdatedEvent($stack->getBoardId()));
$this->enrichStackWithCards($stack); $this->enrichStacksWithCards([$stack]);
return $stack; return $stack;
} }

View File

@@ -129,8 +129,13 @@ class StackServiceTest extends TestCase {
} }
) )
); );
$this->cardMapper->expects($this->any())->method('findAll')->willReturn($this->getCards(222)); $this->cardMapper->expects($this->any())->method('findAllForStacks')->willReturnCallback(function (array $stackIds) {
$r = [];
foreach ($stackIds as $stackId) {
$r[$stackId] = $this->getCards(222);
}
return $r;
});
$actual = $this->stackService->findAll(123); $actual = $this->stackService->findAll(123);
for ($stackId = 0; $stackId < 3; $stackId++) { for ($stackId = 0; $stackId < 3; $stackId++) {
@@ -211,7 +216,7 @@ class StackServiceTest extends TestCase {
$stackToBeDeleted->setBoardId(1); $stackToBeDeleted->setBoardId(1);
$this->stackMapper->expects($this->once())->method('find')->willReturn($stackToBeDeleted); $this->stackMapper->expects($this->once())->method('find')->willReturn($stackToBeDeleted);
$this->stackMapper->expects($this->once())->method('update')->willReturn($stackToBeDeleted); $this->stackMapper->expects($this->once())->method('update')->willReturn($stackToBeDeleted);
$this->cardMapper->expects($this->once())->method('findAll')->willReturn([]); $this->cardMapper->expects($this->once())->method('findAllForStacks')->willReturn([]);
$this->stackService->delete(123); $this->stackService->delete(123);
$this->assertTrue($stackToBeDeleted->getDeletedAt() <= time(), 'deletedAt is in the past'); $this->assertTrue($stackToBeDeleted->getDeletedAt() <= time(), 'deletedAt is in the past');
$this->assertTrue($stackToBeDeleted->getDeletedAt() > 0, 'deletedAt is set'); $this->assertTrue($stackToBeDeleted->getDeletedAt() > 0, 'deletedAt is set');