mirror of
https://github.com/nextcloud/server.git
synced 2026-02-17 01:41:05 -05:00
Merge pull request #54386 from nextcloud/fix-n+1-caldav
fix(performance): Fix n+1 issue when fetching calendar properties
This commit is contained in:
commit
89fa14fd77
9 changed files with 156 additions and 15 deletions
|
|
@ -36,7 +36,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
|
|||
|
||||
public function __construct(
|
||||
BackendInterface $caldavBackend,
|
||||
$calendarInfo,
|
||||
array $calendarInfo,
|
||||
IL10N $l10n,
|
||||
private IConfig $config,
|
||||
private LoggerInterface $logger,
|
||||
|
|
@ -60,6 +60,10 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
|
|||
$this->l10n = $l10n;
|
||||
}
|
||||
|
||||
public function getUri(): string {
|
||||
return $this->calendarInfo['uri'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @throws Forbidden
|
||||
|
|
|
|||
|
|
@ -36,9 +36,14 @@ class CalendarProvider implements ICalendarProvider {
|
|||
});
|
||||
}
|
||||
|
||||
$additionalProperties = $this->getAdditionalPropertiesForCalendars($calendarInfos);
|
||||
$iCalendars = [];
|
||||
foreach ($calendarInfos as $calendarInfo) {
|
||||
$calendarInfo = array_merge($calendarInfo, $this->getAdditionalProperties($calendarInfo['principaluri'], $calendarInfo['uri']));
|
||||
$user = str_replace('principals/users/', '', $calendarInfo['principaluri']);
|
||||
$path = 'calendars/' . $user . '/' . $calendarInfo['uri'];
|
||||
|
||||
$calendarInfo = array_merge($calendarInfo, $additionalProperties[$path] ?? []);
|
||||
|
||||
$calendar = new Calendar($this->calDavBackend, $calendarInfo, $this->l10n, $this->config, $this->logger);
|
||||
$iCalendars[] = new CalendarImpl(
|
||||
$calendar,
|
||||
|
|
@ -49,16 +54,34 @@ class CalendarProvider implements ICalendarProvider {
|
|||
return $iCalendars;
|
||||
}
|
||||
|
||||
public function getAdditionalProperties(string $principalUri, string $calendarUri): array {
|
||||
$user = str_replace('principals/users/', '', $principalUri);
|
||||
$path = 'calendars/' . $user . '/' . $calendarUri;
|
||||
/**
|
||||
* @param array{
|
||||
* principaluri: string,
|
||||
* uri: string,
|
||||
* }[] $uris
|
||||
* @return array<string, array<string, string|bool>>
|
||||
*/
|
||||
private function getAdditionalPropertiesForCalendars(array $uris): array {
|
||||
$calendars = [];
|
||||
foreach ($uris as $uri) {
|
||||
/** @var string $user */
|
||||
$user = str_replace('principals/users/', '', $uri['principaluri']);
|
||||
if (!array_key_exists($user, $calendars)) {
|
||||
$calendars[$user] = [];
|
||||
}
|
||||
$calendars[$user][] = 'calendars/' . $user . '/' . $uri['uri'];
|
||||
}
|
||||
|
||||
$properties = $this->propertyMapper->findPropertiesByPath($user, $path);
|
||||
$properties = $this->propertyMapper->findPropertiesByPathsAndUsers($calendars);
|
||||
|
||||
$list = [];
|
||||
foreach ($properties as $property) {
|
||||
if ($property instanceof Property) {
|
||||
$list[$property->getPropertyname()] = match ($property->getPropertyname()) {
|
||||
if (!isset($list[$property->getPropertypath()])) {
|
||||
$list[$property->getPropertypath()] = [];
|
||||
}
|
||||
|
||||
$list[$property->getPropertypath()][$property->getPropertyname()] = match ($property->getPropertyname()) {
|
||||
'{http://owncloud.org/ns}calendar-enabled' => (bool)$property->getPropertyvalue(),
|
||||
default => $property->getPropertyvalue()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use OCA\DAV\CalDAV\DefaultCalendarValidator;
|
|||
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
|
||||
use OCA\DAV\DAV\CustomPropertiesBackend;
|
||||
use OCA\DAV\DAV\ViewOnlyPlugin;
|
||||
use OCA\DAV\Db\PropertyMapper;
|
||||
use OCA\DAV\Files\BrowserErrorPagePlugin;
|
||||
use OCA\DAV\Files\Sharing\RootCollection;
|
||||
use OCA\DAV\Upload\CleanupService;
|
||||
|
|
@ -226,6 +227,7 @@ class ServerFactory {
|
|||
$tree,
|
||||
$this->databaseConnection,
|
||||
$this->userSession->getUser(),
|
||||
\OCP\Server::get(PropertyMapper::class),
|
||||
\OCP\Server::get(DefaultCalendarValidator::class),
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,13 +9,20 @@
|
|||
namespace OCA\DAV\DAV;
|
||||
|
||||
use Exception;
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\Calendar;
|
||||
use OCA\DAV\CalDAV\CalendarHome;
|
||||
use OCA\DAV\CalDAV\CalendarObject;
|
||||
use OCA\DAV\CalDAV\DefaultCalendarValidator;
|
||||
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
|
||||
use OCA\DAV\CalDAV\Outbox;
|
||||
use OCA\DAV\CalDAV\Trashbin\TrashbinHome;
|
||||
use OCA\DAV\Connector\Sabre\Directory;
|
||||
use OCA\DAV\Db\PropertyMapper;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUser;
|
||||
use Sabre\CalDAV\Schedule\Inbox;
|
||||
use Sabre\DAV\Exception as DavException;
|
||||
use Sabre\DAV\PropertyStorage\Backend\BackendInterface;
|
||||
use Sabre\DAV\PropFind;
|
||||
|
|
@ -98,10 +105,9 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
|
||||
/**
|
||||
* Properties cache
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $userCache = [];
|
||||
private array $userCache = [];
|
||||
private array $publishedCache = [];
|
||||
private XmlService $xmlService;
|
||||
|
||||
/**
|
||||
|
|
@ -114,6 +120,7 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
private Tree $tree,
|
||||
private IDBConnection $connection,
|
||||
private IUser $user,
|
||||
private PropertyMapper $propertyMapper,
|
||||
private DefaultCalendarValidator $defaultCalendarValidator,
|
||||
) {
|
||||
$this->xmlService = new XmlService();
|
||||
|
|
@ -197,6 +204,13 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
$this->cacheDirectory($path, $node);
|
||||
}
|
||||
|
||||
if ($node instanceof CalendarHome && $propFind->getDepth() !== 0) {
|
||||
$backend = $node->getCalDAVBackend();
|
||||
if ($backend instanceof CalDavBackend) {
|
||||
$this->cacheCalendars($node, $requestedProps);
|
||||
}
|
||||
}
|
||||
|
||||
if ($node instanceof CalendarObject) {
|
||||
// No custom properties supported on individual events
|
||||
return;
|
||||
|
|
@ -316,6 +330,10 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
return [];
|
||||
}
|
||||
|
||||
if (isset($this->publishedCache[$path])) {
|
||||
return $this->publishedCache[$path];
|
||||
}
|
||||
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
|
|
@ -326,6 +344,7 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
$props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
|
||||
}
|
||||
$result->closeCursor();
|
||||
$this->publishedCache[$path] = $props;
|
||||
return $props;
|
||||
}
|
||||
|
||||
|
|
@ -364,6 +383,62 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
$this->userCache = array_merge($this->userCache, $propsByPath);
|
||||
}
|
||||
|
||||
private function cacheCalendars(CalendarHome $node, array $requestedProperties): void {
|
||||
$calendars = $node->getChildren();
|
||||
|
||||
$users = [];
|
||||
foreach ($calendars as $calendar) {
|
||||
if ($calendar instanceof Calendar) {
|
||||
$user = str_replace('principals/users/', '', $calendar->getPrincipalURI());
|
||||
if (!isset($users[$user])) {
|
||||
$users[$user] = ['calendars/' . $user];
|
||||
}
|
||||
$users[$user][] = 'calendars/' . $user . '/' . $calendar->getUri();
|
||||
} elseif ($calendar instanceof Inbox || $calendar instanceof Outbox || $calendar instanceof TrashbinHome || $calendar instanceof ExternalCalendar) {
|
||||
if ($calendar->getOwner()) {
|
||||
$user = str_replace('principals/users/', '', $calendar->getOwner());
|
||||
if (!isset($users[$user])) {
|
||||
$users[$user] = ['calendars/' . $user];
|
||||
}
|
||||
$users[$user][] = 'calendars/' . $user . '/' . $calendar->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// user properties
|
||||
$properties = $this->propertyMapper->findPropertiesByPathsAndUsers($users);
|
||||
|
||||
$propsByPath = [];
|
||||
foreach ($users as $paths) {
|
||||
foreach ($paths as $path) {
|
||||
$propsByPath[$path] = [];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($properties as $property) {
|
||||
$propsByPath[$property->getPropertypath()][$property->getPropertyname()] = $this->decodeValueFromDatabase($property->getPropertyvalue(), $property->getValuetype());
|
||||
}
|
||||
$this->userCache = array_merge($this->userCache, $propsByPath);
|
||||
|
||||
// published properties
|
||||
$allowedProps = array_intersect(self::PUBLISHED_READ_ONLY_PROPERTIES, $requestedProperties);
|
||||
if (empty($allowedProps)) {
|
||||
return;
|
||||
}
|
||||
$paths = [];
|
||||
foreach ($users as $nestedPaths) {
|
||||
$paths = array_merge($paths, $nestedPaths);
|
||||
}
|
||||
$paths = array_unique($paths);
|
||||
|
||||
$propsByPath = array_fill_keys(array_values($paths), []);
|
||||
$properties = $this->propertyMapper->findPropertiesByPaths($paths, $allowedProps);
|
||||
foreach ($properties as $property) {
|
||||
$propsByPath[$property->getPropertypath()][$property->getPropertyname()] = $this->decodeValueFromDatabase($property->getPropertyvalue(), $property->getValuetype());
|
||||
}
|
||||
$this->publishedCache = array_merge($this->publishedCache, $propsByPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of properties for the given path and current user
|
||||
*
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use OCP\AppFramework\Db\Entity;
|
|||
* @method string getPropertypath()
|
||||
* @method string getPropertyname()
|
||||
* @method string getPropertyvalue()
|
||||
* @method int getValuetype()
|
||||
*/
|
||||
class Property extends Entity {
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ declare(strict_types=1);
|
|||
namespace OCA\DAV\Db;
|
||||
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
/**
|
||||
|
|
@ -39,17 +40,43 @@ class PropertyMapper extends QBMapper {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string[]> $calendars
|
||||
* @return Property[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findPropertiesByPath(string $userId, string $path): array {
|
||||
public function findPropertiesByPathsAndUsers(array $calendars): array {
|
||||
$selectQb = $this->db->getQueryBuilder();
|
||||
$selectQb->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
->where(
|
||||
$selectQb->expr()->eq('userid', $selectQb->createNamedParameter($userId)),
|
||||
$selectQb->expr()->eq('propertypath', $selectQb->createNamedParameter($path)),
|
||||
->from(self::TABLE_NAME);
|
||||
|
||||
foreach ($calendars as $user => $paths) {
|
||||
$selectQb->orWhere(
|
||||
$selectQb->expr()->andX(
|
||||
$selectQb->expr()->eq('userid', $selectQb->createNamedParameter($user)),
|
||||
$selectQb->expr()->in('propertypath', $selectQb->createNamedParameter($paths, IQueryBuilder::PARAM_STR_ARRAY)),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->findEntities($selectQb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $calendars
|
||||
* @param string[] $allowedProperties
|
||||
* @return Property[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findPropertiesByPaths(array $calendars, array $allowedProperties = []): array {
|
||||
$selectQb = $this->db->getQueryBuilder();
|
||||
$selectQb->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
->where($selectQb->expr()->in('propertypath', $selectQb->createNamedParameter($calendars, IQueryBuilder::PARAM_STR_ARRAY)));
|
||||
|
||||
if ($allowedProperties) {
|
||||
$selectQb->andWhere($selectQb->expr()->in('propertyname', $selectQb->createNamedParameter($allowedProperties, IQueryBuilder::PARAM_STR_ARRAY)));
|
||||
}
|
||||
|
||||
return $this->findEntities($selectQb);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ use OCA\DAV\Connector\Sabre\ZipFolderPlugin;
|
|||
use OCA\DAV\DAV\CustomPropertiesBackend;
|
||||
use OCA\DAV\DAV\PublicAuth;
|
||||
use OCA\DAV\DAV\ViewOnlyPlugin;
|
||||
use OCA\DAV\Db\PropertyMapper;
|
||||
use OCA\DAV\Events\SabrePluginAddEvent;
|
||||
use OCA\DAV\Events\SabrePluginAuthInitEvent;
|
||||
use OCA\DAV\Files\BrowserErrorPagePlugin;
|
||||
|
|
@ -306,6 +307,7 @@ class Server {
|
|||
$this->server->tree,
|
||||
\OCP\Server::get(IDBConnection::class),
|
||||
\OCP\Server::get(IUserSession::class)->getUser(),
|
||||
\OCP\Server::get(PropertyMapper::class),
|
||||
\OCP\Server::get(DefaultCalendarValidator::class),
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use OCA\DAV\CalDAV\DefaultCalendarValidator;
|
|||
use OCA\DAV\Connector\Sabre\Directory;
|
||||
use OCA\DAV\Connector\Sabre\File;
|
||||
use OCA\DAV\DAV\CustomPropertiesBackend;
|
||||
use OCA\DAV\Db\PropertyMapper;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUser;
|
||||
use OCP\Server;
|
||||
|
|
@ -52,6 +53,7 @@ class CustomPropertiesBackendTest extends \Test\TestCase {
|
|||
$this->tree,
|
||||
Server::get(IDBConnection::class),
|
||||
$this->user,
|
||||
Server::get(PropertyMapper::class),
|
||||
$this->defaultCalendarValidator,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ namespace OCA\DAV\Tests\unit\DAV;
|
|||
use OCA\DAV\CalDAV\Calendar;
|
||||
use OCA\DAV\CalDAV\DefaultCalendarValidator;
|
||||
use OCA\DAV\DAV\CustomPropertiesBackend;
|
||||
use OCA\DAV\Db\PropertyMapper;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUser;
|
||||
|
|
@ -36,6 +37,7 @@ class CustomPropertiesBackendTest extends TestCase {
|
|||
private IUser&MockObject $user;
|
||||
private DefaultCalendarValidator&MockObject $defaultCalendarValidator;
|
||||
private CustomPropertiesBackend $backend;
|
||||
private PropertyMapper $propertyMapper;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
|
@ -49,6 +51,7 @@ class CustomPropertiesBackendTest extends TestCase {
|
|||
->with()
|
||||
->willReturn('dummy_user_42');
|
||||
$this->dbConnection = \OCP\Server::get(IDBConnection::class);
|
||||
$this->propertyMapper = \OCP\Server::get(PropertyMapper::class);
|
||||
$this->defaultCalendarValidator = $this->createMock(DefaultCalendarValidator::class);
|
||||
|
||||
$this->backend = new CustomPropertiesBackend(
|
||||
|
|
@ -56,6 +59,7 @@ class CustomPropertiesBackendTest extends TestCase {
|
|||
$this->tree,
|
||||
$this->dbConnection,
|
||||
$this->user,
|
||||
$this->propertyMapper,
|
||||
$this->defaultCalendarValidator,
|
||||
);
|
||||
}
|
||||
|
|
@ -129,6 +133,7 @@ class CustomPropertiesBackendTest extends TestCase {
|
|||
$this->tree,
|
||||
$db,
|
||||
$this->user,
|
||||
$this->propertyMapper,
|
||||
$this->defaultCalendarValidator,
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue