diff --git a/lib/Db/BulkFetchTrait.php b/lib/Db/BulkFetchTrait.php new file mode 100644 index 000000000..1d584398f --- /dev/null +++ b/lib/Db/BulkFetchTrait.php @@ -0,0 +1,23 @@ +db->getDatabaseProvider()) { + IDBConnection::PLATFORM_ORACLE => 1000, + default => 65_535, + }; + return $maxParameters - $extraParameters; + } +} diff --git a/lib/Db/ShareMapper.php b/lib/Db/ShareMapper.php index 959bcd96b..e41af79de 100644 --- a/lib/Db/ShareMapper.php +++ b/lib/Db/ShareMapper.php @@ -18,6 +18,8 @@ /** @template-extends QBMapper */ class ShareMapper extends QBMapper { + use BulkFetchTrait; + protected string $table = 'tables_shares'; protected LoggerInterface $logger; @@ -91,23 +93,34 @@ public function findShareForNode(int $nodeId, string $nodeType, string $receiver /** * @param string $nodeType - * @param string $receiver + * @param string[]|int[] $receivers * @param string $userId * @param string|null $receiverType * - * @return array + * @return Share[] * * @throws Exception */ - public function findAllSharesFor(string $nodeType, string $receiver, string $userId, ?string $receiverType = 'user'): array { - $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from($this->table) - ->where($qb->expr()->eq('receiver', $qb->createNamedParameter($receiver, IQueryBuilder::PARAM_STR))) - ->andWhere($qb->expr()->neq('sender', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))) - ->andWhere($qb->expr()->eq('node_type', $qb->createNamedParameter($nodeType, IQueryBuilder::PARAM_STR))) - ->andWhere($qb->expr()->eq('receiver_type', $qb->createNamedParameter($receiverType, IQueryBuilder::PARAM_STR))); - return $this->findEntities($qb); + public function findAllSharesFor(string $nodeType, array $receivers, string $userId, ?string $receiverType = 'user'): array { + if (!$receivers) { + return []; + } + + $chunks = []; + // deduct extra parameters (sender, node type, receiver type) + foreach (array_chunk($receivers, $this->getChunkSize(3)) as $receiversChunk) { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->table) + ->where($qb->expr()->in('receiver', $qb->createNamedParameter($receiversChunk, IQueryBuilder::PARAM_STR_ARRAY))) + ->andWhere($qb->expr()->neq('sender', $qb->createNamedParameter($userId))) + ->andWhere($qb->expr()->eq('node_type', $qb->createNamedParameter($nodeType))) + ->andWhere($qb->expr()->eq('receiver_type', $qb->createNamedParameter($receiverType))); + + $chunks[] = $this->findEntities($qb); + } + + return array_merge(...$chunks); } /** diff --git a/lib/Db/TableMapper.php b/lib/Db/TableMapper.php index 9c094bcd8..7c0ac49f6 100644 --- a/lib/Db/TableMapper.php +++ b/lib/Db/TableMapper.php @@ -19,6 +19,8 @@ /** @template-extends QBMapper */ class TableMapper extends QBMapper { + use BulkFetchTrait; + protected string $table = 'tables_tables'; protected CappedMemoryCache $cache; public function __construct( @@ -50,6 +52,42 @@ public function find(int $id): Table { return $this->cache[$cacheKey]; } + /** + * @param int[] $ids + * @return array indexed by table id + */ + public function findMany(array $ids): array { + $missing = []; + $result = []; + foreach ($ids as $id) { + $cacheKey = (string)$id; + if (isset($this->cache[$cacheKey])) { + $result[$id] = $this->cache[$cacheKey]; + } else { + $missing[$id] = true; + } + } + + if (!$missing) { + return $result; + } + + $missing = array_keys($missing); + foreach (array_chunk($missing, $this->getChunkSize()) as $missingChunk) { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->table) + ->where($qb->expr()->in('id', $qb->createNamedParameter($missingChunk, IQueryBuilder::PARAM_INT_ARRAY))); + + foreach ($this->findEntities($qb) as $entity) { + $id = $entity->getId(); + $this->cache[(string)$id] = $entity; + $result[$id] = $entity; + } + } + return $result; + } + /** * @param string|null $userId * @return Table[] diff --git a/lib/Db/ViewMapper.php b/lib/Db/ViewMapper.php index e6d279d7e..b4cde66a3 100644 --- a/lib/Db/ViewMapper.php +++ b/lib/Db/ViewMapper.php @@ -19,6 +19,8 @@ /** @template-extends QBMapper */ class ViewMapper extends QBMapper { + use BulkFetchTrait; + protected string $table = 'tables_views'; protected CappedMemoryCache $cache; @@ -50,6 +52,43 @@ public function find(int $id): View { return $this->cache[$cacheKey]; } + /** + * @param int[] $ids + * @return array indexed by view id + */ + public function findMany(array $ids): array { + $missing = []; + $result = []; + foreach ($ids as $id) { + $cacheKey = (string)$id; + if (isset($this->cache[$cacheKey])) { + $result[$id] = $this->cache[$cacheKey]; + } else { + $missing[$id] = true; + } + } + + if (!$missing) { + return $result; + } + + $missing = array_keys($missing); + foreach (array_chunk($missing, $this->getChunkSize()) as $missingChunk) { + $qb = $this->db->getQueryBuilder(); + $qb->select('v.*', 't.ownership') + ->from($this->table, 'v') + ->innerJoin('v', 'tables_tables', 't', 't.id = v.table_id') + ->where($qb->expr()->in('v.id', $qb->createNamedParameter($missingChunk, IQueryBuilder::PARAM_INT_ARRAY))); + + foreach ($this->findEntities($qb) as $entity) { + $id = $entity->getId(); + $this->cache[(string)$id] = $entity; + $result[$id] = $entity; + } + } + return $result; + } + public function delete(Entity $entity): View { unset($this->cache[(string)$entity->getId()]); return parent::delete($entity); diff --git a/lib/Helper/CircleHelper.php b/lib/Helper/CircleHelper.php index f9ee8c254..b47f025d5 100644 --- a/lib/Helper/CircleHelper.php +++ b/lib/Helper/CircleHelper.php @@ -76,7 +76,6 @@ public function getCircleDisplayName(string $circleId, string $userId): string { /** * @param string $userId * @return Circle[] - * @throws InternalError */ public function getUserCircles(string $userId): array { if (!$this->circlesEnabled) { diff --git a/lib/Service/ShareService.php b/lib/Service/ShareService.php index a66447859..f04d5dfc1 100644 --- a/lib/Service/ShareService.php +++ b/lib/Service/ShareService.php @@ -13,6 +13,7 @@ use InvalidArgumentException; use OC\User\User; +use OCA\Circles\Model\Circle; use OCA\Tables\AppInfo\Application; use OCA\Tables\Constants\ShareReceiverType; use OCA\Tables\Db\Context; @@ -39,6 +40,7 @@ use OCP\AppFramework\Db\TTransactional; use OCP\DB\Exception; use OCP\IDBConnection; +use OCP\IGroup; use OCP\IUserManager; use OCP\Security\ISecureRandom; use Psr\Log\LoggerInterface; @@ -181,7 +183,7 @@ protected function generateShareToken(): ShareToken { /** * @param string|null $userId - * @return array + * @return array Indexed by view id * @throws InternalError */ public function findViewsSharedWithMe(?string $userId = null): array { @@ -190,7 +192,7 @@ public function findViewsSharedWithMe(?string $userId = null): array { /** * @param string|null $userId - * @return array + * @return array Indexed by table id * @throws InternalError */ public function findTablesSharedWithMe(?string $userId = null): array { @@ -203,52 +205,38 @@ public function findTablesSharedWithMe(?string $userId = null): array { private function findElementsSharedWithMe(string $elementType = 'table', ?string $userId = null): array { $userId = $this->permissionsService->preCheckUserId($userId); - $returnArray = []; - + $shares = []; try { - // get all views or tables that are shared with me as user - $elementsSharedWithMe = $this->mapper->findAllSharesFor($elementType, $userId, $userId); + $shares['user'] = $this->mapper->findAllSharesFor($elementType, [$userId], $userId); - // get all views or tables that are shared with me by group $userGroups = $this->userHelper->getGroupsForUser($userId); - foreach ($userGroups as $userGroup) { - $shares = $this->mapper->findAllSharesFor($elementType, $userGroup->getGid(), $userId, ShareReceiverType::GROUP); - $elementsSharedWithMe = array_merge($elementsSharedWithMe, $shares); - } - - // get all views or tables that are shared with me by circle - if ($this->circleHelper->isCirclesEnabled()) { - $userCircles = $this->circleHelper->getUserCircles($userId); + $userGroupIds = array_map(static fn (IGroup $group) => $group->getGid(), $userGroups); + $shares['groups'] = $this->mapper->findAllSharesFor($elementType, $userGroupIds, $userId, ShareReceiverType::GROUP); - foreach ($userCircles as $userCircle) { - $shares = $this->mapper->findAllSharesFor($elementType, $userCircle->getSingleId(), $userId, ShareReceiverType::CIRCLE); - $elementsSharedWithMe = array_merge($elementsSharedWithMe, $shares); - } - } + $userCircles = $this->circleHelper->getUserCircles($userId); + $userCircleIds = array_map(static fn (Circle $circle) => $circle->getSingleId(), $userCircles); + $shares['circles'] = $this->mapper->findAllSharesFor($elementType, $userCircleIds, $userId, ShareReceiverType::CIRCLE); } catch (Throwable $e) { - throw new InternalError($e->getMessage()); + throw new InternalError($e->getMessage(), previous: $e); } - foreach ($elementsSharedWithMe as $share) { - try { - if ($elementType === 'table') { - $element = $this->tableMapper->find($share->getNodeId()); - } elseif ($elementType === 'view') { - $element = $this->viewMapper->find($share->getNodeId()); - } else { - throw new InternalError('Cannot find element of type ' . $elementType); - } - // Check if en element with this id is already in the result array - $index = array_search($element->getId(), array_column($returnArray, 'id')); - if (!$index) { - $returnArray[] = $element; - } - } catch (DoesNotExistException|Exception|MultipleObjectsReturnedException $e) { - throw new InternalError($e->getMessage()); - } + + $elementIds = []; + foreach (array_merge($shares['user'], $shares['groups'], $shares['circles']) as $share) { + /** @var Share $share */ + $elementIds[$share->getNodeId()] = true; + } + $elementIds = array_keys($elementIds); + + if ($elementType === 'table') { + return $this->tableMapper->findMany($elementIds); } - return $returnArray; - } + if ($elementType === 'view') { + return $this->viewMapper->findMany($elementIds); + } + + throw new InternalError('Cannot find element of type ' . $elementType); + } /** * @param int $elementId @@ -286,7 +274,6 @@ public function create(int $nodeId, string $nodeType, string $receiver, string $ $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } - $time = new DateTime(); $item = new Share(); $item->setSender($sender); diff --git a/lib/Service/ViewService.php b/lib/Service/ViewService.php index 8d6dfd22d..cc05fc5b8 100644 --- a/lib/Service/ViewService.php +++ b/lib/Service/ViewService.php @@ -163,30 +163,24 @@ public function findSharedViewsWithMe(?string $userId = null): array { return []; } - $allViews = []; - $sharedViews = $this->shareService->findViewsSharedWithMe($userId); - foreach ($sharedViews as $sharedView) { - $allViews[$sharedView->getId()] = $sharedView; - } - $contexts = $this->contextService->findAll($userId); foreach ($contexts as $context) { $nodes = $context->getNodes(); foreach ($nodes as $node) { if ($node['node_type'] !== Application::NODE_TYPE_VIEW - || isset($allViews[$node['node_id']]) + || isset($sharedViews[$node['node_id']]) ) { continue; } - $allViews[$node['node_id']] = $this->find($node['node_id'], false, $userId); + $sharedViews[$node['node_id']] = $this->find($node['node_id'], false, $userId); } } - foreach ($allViews as $view) { + foreach ($sharedViews as $view) { $this->enhanceView($view, $userId); } - return array_values($allViews); + return array_values($sharedViews); }