diff options
Diffstat (limited to 'lib/public/AppFramework/Db/QBMapper.php')
-rw-r--r-- | lib/public/AppFramework/Db/QBMapper.php | 377 |
1 files changed, 377 insertions, 0 deletions
diff --git a/lib/public/AppFramework/Db/QBMapper.php b/lib/public/AppFramework/Db/QBMapper.php new file mode 100644 index 00000000000..7fb5b2a9afd --- /dev/null +++ b/lib/public/AppFramework/Db/QBMapper.php @@ -0,0 +1,377 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCP\AppFramework\Db; + +use Generator; +use OCP\DB\Exception; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DB\Types; +use OCP\IDBConnection; + +/** + * Simple parent class for inheriting your data access layer from. This class + * may be subject to change in the future + * + * @since 14.0.0 + * + * @template T of Entity + */ +abstract class QBMapper { + /** @var string */ + protected $tableName; + + /** @var string|class-string<T> */ + protected $entityClass; + + /** @var IDBConnection */ + protected $db; + + /** + * @param IDBConnection $db Instance of the Db abstraction layer + * @param string $tableName the name of the table. set this to allow entity + * @param class-string<T>|null $entityClass the name of the entity that the sql should be + * mapped to queries without using sql + * @since 14.0.0 + */ + public function __construct(IDBConnection $db, string $tableName, ?string $entityClass = null) { + $this->db = $db; + $this->tableName = $tableName; + + // if not given set the entity name to the class without the mapper part + // cache it here for later use since reflection is slow + if ($entityClass === null) { + $this->entityClass = str_replace('Mapper', '', \get_class($this)); + } else { + $this->entityClass = $entityClass; + } + } + + + /** + * @return string the table name + * @since 14.0.0 + */ + public function getTableName(): string { + return $this->tableName; + } + + + /** + * Deletes an entity from the table + * + * @param Entity $entity the entity that should be deleted + * @psalm-param T $entity the entity that should be deleted + * @return Entity the deleted entity + * @psalm-return T the deleted entity + * @throws Exception + * @since 14.0.0 + */ + public function delete(Entity $entity): Entity { + $qb = $this->db->getQueryBuilder(); + + $idType = $this->getParameterTypeForProperty($entity, 'id'); + + $qb->delete($this->tableName) + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($entity->getId(), $idType)) + ); + $qb->executeStatement(); + return $entity; + } + + + /** + * Creates a new entry in the db from an entity + * + * @param Entity $entity the entity that should be created + * @psalm-param T $entity the entity that should be created + * @return Entity the saved entity with the set id + * @psalm-return T the saved entity with the set id + * @throws Exception + * @since 14.0.0 + */ + public function insert(Entity $entity): Entity { + // get updated fields to save, fields have to be set using a setter to + // be saved + $properties = $entity->getUpdatedFields(); + + $qb = $this->db->getQueryBuilder(); + $qb->insert($this->tableName); + + // build the fields + foreach ($properties as $property => $updated) { + $column = $entity->propertyToColumn($property); + $getter = 'get' . ucfirst($property); + $value = $entity->$getter(); + + $type = $this->getParameterTypeForProperty($entity, $property); + $qb->setValue($column, $qb->createNamedParameter($value, $type)); + } + + $qb->executeStatement(); + + if ($entity->id === null) { + // When autoincrement is used id is always an int + $entity->setId($qb->getLastInsertId()); + } + + return $entity; + } + + /** + * Tries to creates a new entry in the db from an entity and + * updates an existing entry if duplicate keys are detected + * by the database + * + * @param Entity $entity the entity that should be created/updated + * @psalm-param T $entity the entity that should be created/updated + * @return Entity the saved entity with the (new) id + * @psalm-return T the saved entity with the (new) id + * @throws Exception + * @throws \InvalidArgumentException if entity has no id + * @since 15.0.0 + */ + public function insertOrUpdate(Entity $entity): Entity { + try { + return $this->insert($entity); + } catch (Exception $ex) { + if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + return $this->update($entity); + } + throw $ex; + } + } + + /** + * Updates an entry in the db from an entity + * + * @param Entity $entity the entity that should be created + * @psalm-param T $entity the entity that should be created + * @return Entity the saved entity with the set id + * @psalm-return T the saved entity with the set id + * @throws Exception + * @throws \InvalidArgumentException if entity has no id + * @since 14.0.0 + */ + public function update(Entity $entity): Entity { + // if entity wasn't changed it makes no sense to run a db query + $properties = $entity->getUpdatedFields(); + if (\count($properties) === 0) { + return $entity; + } + + // entity needs an id + $id = $entity->getId(); + if ($id === null) { + throw new \InvalidArgumentException( + 'Entity which should be updated has no id'); + } + + // get updated fields to save, fields have to be set using a setter to + // be saved + // do not update the id field + unset($properties['id']); + + $qb = $this->db->getQueryBuilder(); + $qb->update($this->tableName); + + // build the fields + foreach ($properties as $property => $updated) { + $column = $entity->propertyToColumn($property); + $getter = 'get' . ucfirst($property); + $value = $entity->$getter(); + + $type = $this->getParameterTypeForProperty($entity, $property); + $qb->set($column, $qb->createNamedParameter($value, $type)); + } + + $idType = $this->getParameterTypeForProperty($entity, 'id'); + + $qb->where( + $qb->expr()->eq('id', $qb->createNamedParameter($id, $idType)) + ); + $qb->executeStatement(); + + return $entity; + } + + /** + * Returns the type parameter for the QueryBuilder for a specific property + * of the $entity + * + * @param Entity $entity The entity to get the types from + * @psalm-param T $entity + * @param string $property The property of $entity to get the type for + * @return int|string + * @since 16.0.0 + */ + protected function getParameterTypeForProperty(Entity $entity, string $property) { + $types = $entity->getFieldTypes(); + + if (!isset($types[ $property ])) { + return IQueryBuilder::PARAM_STR; + } + + switch ($types[ $property ]) { + case 'int': + case Types::INTEGER: + case Types::SMALLINT: + return IQueryBuilder::PARAM_INT; + case Types::STRING: + return IQueryBuilder::PARAM_STR; + case 'bool': + case Types::BOOLEAN: + return IQueryBuilder::PARAM_BOOL; + case Types::BLOB: + return IQueryBuilder::PARAM_LOB; + case Types::DATE: + return IQueryBuilder::PARAM_DATETIME_MUTABLE; + case Types::DATETIME: + return IQueryBuilder::PARAM_DATETIME_MUTABLE; + case Types::DATETIME_TZ: + return IQueryBuilder::PARAM_DATETIME_TZ_MUTABLE; + case Types::DATE_IMMUTABLE: + return IQueryBuilder::PARAM_DATE_IMMUTABLE; + case Types::DATETIME_IMMUTABLE: + return IQueryBuilder::PARAM_DATETIME_IMMUTABLE; + case Types::DATETIME_TZ_IMMUTABLE: + return IQueryBuilder::PARAM_DATETIME_TZ_IMMUTABLE; + case Types::TIME: + return IQueryBuilder::PARAM_TIME_MUTABLE; + case Types::TIME_IMMUTABLE: + return IQueryBuilder::PARAM_TIME_IMMUTABLE; + case Types::JSON: + return IQueryBuilder::PARAM_JSON; + } + + return IQueryBuilder::PARAM_STR; + } + + /** + * Returns an db result and throws exceptions when there are more or less + * results + * + * @param IQueryBuilder $query + * @return array the result as row + * @throws Exception + * @throws MultipleObjectsReturnedException if more than one item exist + * @throws DoesNotExistException if the item does not exist + * @see findEntity + * + * @since 14.0.0 + */ + protected function findOneQuery(IQueryBuilder $query): array { + $result = $query->executeQuery(); + + $row = $result->fetch(); + if ($row === false) { + $result->closeCursor(); + $msg = $this->buildDebugMessage( + 'Did expect one result but found none when executing', $query + ); + throw new DoesNotExistException($msg); + } + + $row2 = $result->fetch(); + $result->closeCursor(); + if ($row2 !== false) { + $msg = $this->buildDebugMessage( + 'Did not expect more than one result when executing', $query + ); + throw new MultipleObjectsReturnedException($msg); + } + + return $row; + } + + /** + * @param string $msg + * @param IQueryBuilder $sql + * @return string + * @since 14.0.0 + */ + private function buildDebugMessage(string $msg, IQueryBuilder $sql): string { + return $msg + . ': query "' . $sql->getSQL() . '"; '; + } + + + /** + * Creates an entity from a row. Automatically determines the entity class + * from the current mapper name (MyEntityMapper -> MyEntity) + * + * @param array $row the row which should be converted to an entity + * @return Entity the entity + * @psalm-return T the entity + * @since 14.0.0 + */ + protected function mapRowToEntity(array $row): Entity { + unset($row['DOCTRINE_ROWNUM']); // remove doctrine/dbal helper column + return \call_user_func($this->entityClass . '::fromRow', $row); + } + + + /** + * Runs a sql query and returns an array of entities + * + * @param IQueryBuilder $query + * @return list<Entity> all fetched entities + * @psalm-return list<T> all fetched entities + * @throws Exception + * @since 14.0.0 + */ + protected function findEntities(IQueryBuilder $query): array { + $result = $query->executeQuery(); + try { + $entities = []; + while ($row = $result->fetch()) { + $entities[] = $this->mapRowToEntity($row); + } + return $entities; + } finally { + $result->closeCursor(); + } + } + + /** + * Runs a sql query and yields each resulting entity to obtain database entries in a memory-efficient way + * + * @param IQueryBuilder $query + * @return Generator Generator of fetched entities + * @psalm-return Generator<T> Generator of fetched entities + * @throws Exception + * @since 30.0.0 + */ + protected function yieldEntities(IQueryBuilder $query): Generator { + $result = $query->executeQuery(); + try { + while ($row = $result->fetch()) { + yield $this->mapRowToEntity($row); + } + } finally { + $result->closeCursor(); + } + } + + + /** + * Returns an db result and throws exceptions when there are more or less + * results + * + * @param IQueryBuilder $query + * @return Entity the entity + * @psalm-return T the entity + * @throws Exception + * @throws MultipleObjectsReturnedException if more than one item exist + * @throws DoesNotExistException if the item does not exist + * @since 14.0.0 + */ + protected function findEntity(IQueryBuilder $query): Entity { + return $this->mapRowToEntity($this->findOneQuery($query)); + } +} |