Skip to content

Commit 5d8cc67

Browse files
authored
Merge pull request #2293 from nextcloud/feature/optimize-shares-loading
Perf: Optimize shares loading by eliminating N+1 queries
2 parents cabc8a2 + da356e9 commit 5d8cc67

6 files changed

Lines changed: 127 additions & 62 deletions

File tree

lib/Db/ShareMapper.php

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,23 +84,34 @@ public function findShareForNode(int $nodeId, string $nodeType, string $receiver
8484

8585
/**
8686
* @param string $nodeType
87-
* @param string $receiver
87+
* @param string[]|int[] $receivers
8888
* @param string $userId
8989
* @param string|null $receiverType
9090
*
91-
* @return array
91+
* @return Share[]
9292
*
9393
* @throws Exception
9494
*/
95-
public function findAllSharesFor(string $nodeType, string $receiver, string $userId, ?string $receiverType = 'user'): array {
96-
$qb = $this->db->getQueryBuilder();
97-
$qb->select('*')
98-
->from($this->table)
99-
->where($qb->expr()->eq('receiver', $qb->createNamedParameter($receiver, IQueryBuilder::PARAM_STR)))
100-
->andWhere($qb->expr()->neq('sender', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
101-
->andWhere($qb->expr()->eq('node_type', $qb->createNamedParameter($nodeType, IQueryBuilder::PARAM_STR)))
102-
->andWhere($qb->expr()->eq('receiver_type', $qb->createNamedParameter($receiverType, IQueryBuilder::PARAM_STR)));
103-
return $this->findEntities($qb);
95+
public function findAllSharesFor(string $nodeType, array $receivers, string $userId, ?string $receiverType = 'user'): array {
96+
if (!$receivers) {
97+
return [];
98+
}
99+
100+
$chunks = [];
101+
// deduct extra parameters (sender, node type, receiver type)
102+
foreach (array_chunk($receivers, 1000 - 3) as $receiversChunk) {
103+
$qb = $this->db->getQueryBuilder();
104+
$qb->select('*')
105+
->from($this->table)
106+
->where($qb->expr()->in('receiver', $qb->createNamedParameter($receiversChunk, IQueryBuilder::PARAM_STR_ARRAY)))
107+
->andWhere($qb->expr()->neq('sender', $qb->createNamedParameter($userId)))
108+
->andWhere($qb->expr()->eq('node_type', $qb->createNamedParameter($nodeType)))
109+
->andWhere($qb->expr()->eq('receiver_type', $qb->createNamedParameter($receiverType)));
110+
111+
$chunks[] = $this->findEntities($qb);
112+
}
113+
114+
return array_merge(...$chunks);
104115
}
105116

106117
/**

lib/Db/TableMapper.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,42 @@ public function find(int $id): Table {
5050
return $this->cache[$cacheKey];
5151
}
5252

53+
/**
54+
* @param int[] $ids
55+
* @return array<int, Table> indexed by table id
56+
*/
57+
public function findMany(array $ids): array {
58+
$missing = [];
59+
$result = [];
60+
foreach ($ids as $id) {
61+
$cacheKey = (string)$id;
62+
if (isset($this->cache[$cacheKey])) {
63+
$result[$id] = $this->cache[$cacheKey];
64+
} else {
65+
$missing[$id] = true;
66+
}
67+
}
68+
69+
if (!$missing) {
70+
return $result;
71+
}
72+
73+
$missing = array_keys($missing);
74+
foreach (array_chunk($missing, 1000) as $missingChunk) {
75+
$qb = $this->db->getQueryBuilder();
76+
$qb->select('*')
77+
->from($this->table)
78+
->where($qb->expr()->in('id', $qb->createNamedParameter($missingChunk, IQueryBuilder::PARAM_INT_ARRAY)));
79+
80+
foreach ($this->findEntities($qb) as $entity) {
81+
$id = $entity->getId();
82+
$this->cache[(string)$id] = $entity;
83+
$result[$id] = $entity;
84+
}
85+
}
86+
return $result;
87+
}
88+
5389
/**
5490
* @param string|null $userId
5591
* @return Table[]

lib/Db/ViewMapper.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,43 @@ public function find(int $id): View {
5050
return $this->cache[$cacheKey];
5151
}
5252

53+
/**
54+
* @param int[] $ids
55+
* @return array<int, View> indexed by view id
56+
*/
57+
public function findMany(array $ids): array {
58+
$missing = [];
59+
$result = [];
60+
foreach ($ids as $id) {
61+
$cacheKey = (string)$id;
62+
if (isset($this->cache[$cacheKey])) {
63+
$result[$id] = $this->cache[$cacheKey];
64+
} else {
65+
$missing[$id] = true;
66+
}
67+
}
68+
69+
if (!$missing) {
70+
return $result;
71+
}
72+
73+
$missing = array_keys($missing);
74+
foreach (array_chunk($missing, 1000) as $missingChunk) {
75+
$qb = $this->db->getQueryBuilder();
76+
$qb->select('v.*', 't.ownership')
77+
->from($this->table, 'v')
78+
->innerJoin('v', 'tables_tables', 't', 't.id = v.table_id')
79+
->where($qb->expr()->in('v.id', $qb->createNamedParameter($missingChunk, IQueryBuilder::PARAM_INT_ARRAY)));
80+
81+
foreach ($this->findEntities($qb) as $entity) {
82+
$id = $entity->getId();
83+
$this->cache[(string)$id] = $entity;
84+
$result[$id] = $entity;
85+
}
86+
}
87+
return $result;
88+
}
89+
5390
public function delete(Entity $entity): View {
5491
unset($this->cache[(string)$entity->getId()]);
5592
return parent::delete($entity);

lib/Helper/CircleHelper.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ public function getCircleDisplayName(string $circleId, string $userId): string {
7676
/**
7777
* @param string $userId
7878
* @return Circle[]
79-
* @throws InternalError
8079
*/
8180
public function getUserCircles(string $userId): array {
8281
if (!$this->circlesEnabled) {

lib/Service/ShareService.php

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use DateTime;
1313

1414
use InvalidArgumentException;
15+
use OCA\Circles\Model\Circle;
1516
use OCA\Tables\AppInfo\Application;
1617
use OCA\Tables\Constants\ShareReceiverType;
1718
use OCA\Tables\Db\Context;
@@ -38,6 +39,7 @@
3839
use OCP\AppFramework\Db\TTransactional;
3940
use OCP\DB\Exception;
4041
use OCP\IDBConnection;
42+
use OCP\IGroup;
4143
use OCP\IUserManager;
4244
use OCP\Security\IHasher;
4345
use OCP\Security\ISecureRandom;
@@ -194,7 +196,7 @@ protected function generateShareToken(): ShareToken {
194196

195197
/**
196198
* @param string|null $userId
197-
* @return array
199+
* @return array<int, View> Indexed by view id
198200
* @throws InternalError
199201
*/
200202
public function findViewsSharedWithMe(?string $userId = null): array {
@@ -203,7 +205,7 @@ public function findViewsSharedWithMe(?string $userId = null): array {
203205

204206
/**
205207
* @param string|null $userId
206-
* @return array
208+
* @return array<int, Table> Indexed by table id
207209
* @throws InternalError
208210
*/
209211
public function findTablesSharedWithMe(?string $userId = null): array {
@@ -216,52 +218,38 @@ public function findTablesSharedWithMe(?string $userId = null): array {
216218
private function findElementsSharedWithMe(string $elementType = 'table', ?string $userId = null): array {
217219
$userId = $this->permissionsService->preCheckUserId($userId);
218220

219-
$returnArray = [];
220-
221+
$shares = [];
221222
try {
222-
// get all views or tables that are shared with me as user
223-
$elementsSharedWithMe = $this->mapper->findAllSharesFor($elementType, $userId, $userId);
223+
$shares['user'] = $this->mapper->findAllSharesFor($elementType, [$userId], $userId);
224224

225-
// get all views or tables that are shared with me by group
226225
$userGroups = $this->userHelper->getGroupsForUser($userId);
227-
foreach ($userGroups as $userGroup) {
228-
$shares = $this->mapper->findAllSharesFor($elementType, $userGroup->getGid(), $userId, ShareReceiverType::GROUP);
229-
$elementsSharedWithMe = array_merge($elementsSharedWithMe, $shares);
230-
}
226+
$userGroupIds = array_map(static fn (IGroup $group) => $group->getGid(), $userGroups);
227+
$shares['groups'] = $this->mapper->findAllSharesFor($elementType, $userGroupIds, $userId, ShareReceiverType::GROUP);
231228

232-
// get all views or tables that are shared with me by circle
233-
if ($this->circleHelper->isCirclesEnabled()) {
234-
$userCircles = $this->circleHelper->getUserCircles($userId);
235-
236-
foreach ($userCircles as $userCircle) {
237-
$shares = $this->mapper->findAllSharesFor($elementType, $userCircle->getSingleId(), $userId, ShareReceiverType::CIRCLE);
238-
$elementsSharedWithMe = array_merge($elementsSharedWithMe, $shares);
239-
}
240-
}
229+
$userCircles = $this->circleHelper->getUserCircles($userId);
230+
$userCircleIds = array_map(static fn (Circle $circle) => $circle->getSingleId(), $userCircles);
231+
$shares['circles'] = $this->mapper->findAllSharesFor($elementType, $userCircleIds, $userId, ShareReceiverType::CIRCLE);
241232
} catch (Throwable $e) {
242-
throw new InternalError($e->getMessage());
233+
throw new InternalError($e->getMessage(), previous: $e);
243234
}
244-
foreach ($elementsSharedWithMe as $share) {
245-
try {
246-
if ($elementType === 'table') {
247-
$element = $this->tableMapper->find($share->getNodeId());
248-
} elseif ($elementType === 'view') {
249-
$element = $this->viewMapper->find($share->getNodeId());
250-
} else {
251-
throw new InternalError('Cannot find element of type ' . $elementType);
252-
}
253-
// Check if en element with this id is already in the result array
254-
$index = array_search($element->getId(), array_column($returnArray, 'id'), true);
255-
if (!$index) {
256-
$returnArray[] = $element;
257-
}
258-
} catch (DoesNotExistException|Exception|MultipleObjectsReturnedException $e) {
259-
throw new InternalError($e->getMessage());
260-
}
235+
236+
$elementIds = [];
237+
foreach (array_merge($shares['user'], $shares['groups'], $shares['circles']) as $share) {
238+
/** @var Share $share */
239+
$elementIds[$share->getNodeId()] = true;
261240
}
262-
return $returnArray;
263-
}
241+
$elementIds = array_keys($elementIds);
264242

243+
if ($elementType === 'table') {
244+
return $this->tableMapper->findMany($elementIds);
245+
}
246+
247+
if ($elementType === 'view') {
248+
return $this->viewMapper->findMany($elementIds);
249+
}
250+
251+
throw new InternalError('Cannot find element of type ' . $elementType);
252+
}
265253

266254
/**
267255
* @param int $elementId

lib/Service/ViewService.php

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -163,30 +163,24 @@ public function findSharedViewsWithMe(?string $userId = null): array {
163163
return [];
164164
}
165165

166-
$allViews = [];
167-
168166
$sharedViews = $this->shareService->findViewsSharedWithMe($userId);
169-
foreach ($sharedViews as $sharedView) {
170-
$allViews[$sharedView->getId()] = $sharedView;
171-
}
172-
173167
$contexts = $this->contextService->findAll($userId);
174168
foreach ($contexts as $context) {
175169
$nodes = $context->getNodes();
176170
foreach ($nodes as $node) {
177171
if ($node['node_type'] !== Application::NODE_TYPE_VIEW
178-
|| isset($allViews[$node['node_id']])
172+
|| isset($sharedViews[$node['node_id']])
179173
) {
180174
continue;
181175
}
182-
$allViews[$node['node_id']] = $this->find($node['node_id'], false, $userId);
176+
$sharedViews[$node['node_id']] = $this->find($node['node_id'], false, $userId);
183177
}
184178
}
185179

186-
foreach ($allViews as $view) {
180+
foreach ($sharedViews as $view) {
187181
$this->enhanceView($view, $userId);
188182
}
189-
return array_values($allViews);
183+
return array_values($sharedViews);
190184
}
191185

192186

0 commit comments

Comments
 (0)