From 74d7f6d4ca84d81de873611aafd64832e0715e0b Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Thu, 10 May 2018 14:35:26 +0200 Subject: [PATCH] Add a QueryBuilder Mapper Signed-off-by: Roeland Jago Douma --- lib/public/AppFramework/Db/Mapper.php | 15 +- lib/public/AppFramework/Db/QBMapper.php | 270 ++++++++++++++++++++++++ 2 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 lib/public/AppFramework/Db/QBMapper.php diff --git a/lib/public/AppFramework/Db/Mapper.php b/lib/public/AppFramework/Db/Mapper.php index b008702ba5..6910757add 100644 --- a/lib/public/AppFramework/Db/Mapper.php +++ b/lib/public/AppFramework/Db/Mapper.php @@ -34,6 +34,7 @@ use OCP\IDBConnection; * Simple parent class for inheriting your data access layer from. This class * may be subject to change in the future * @since 7.0.0 + * @deprecated 14.0.0 Move over to QBMapper */ abstract class Mapper { @@ -47,6 +48,7 @@ abstract class Mapper { * @param string $entityClass the name of the entity that the sql should be * mapped to queries without using sql * @since 7.0.0 + * @deprecated 14.0.0 Move over to QBMapper */ public function __construct(IDBConnection $db, $tableName, $entityClass=null){ $this->db = $db; @@ -65,6 +67,7 @@ abstract class Mapper { /** * @return string the table name * @since 7.0.0 + * @deprecated 14.0.0 Move over to QBMapper */ public function getTableName(){ return $this->tableName; @@ -76,6 +79,7 @@ abstract class Mapper { * @param Entity $entity the entity that should be deleted * @return Entity the deleted entity * @since 7.0.0 - return value added in 8.1.0 + * @deprecated 14.0.0 Move over to QBMapper */ public function delete(Entity $entity){ $sql = 'DELETE FROM `' . $this->tableName . '` WHERE `id` = ?'; @@ -90,6 +94,7 @@ abstract class Mapper { * @param Entity $entity the entity that should be created * @return Entity the saved entity with the set id * @since 7.0.0 + * @deprecated 14.0.0 Move over to QBMapper */ public function insert(Entity $entity){ // get updated fields to save, fields have to be set using a setter to @@ -139,6 +144,7 @@ abstract class Mapper { * @param Entity $entity the entity that should be created * @return Entity the saved entity with the set id * @since 7.0.0 - return value was added in 8.0.0 + * @deprecated 14.0.0 Move over to QBMapper */ public function update(Entity $entity){ // if entity wasn't changed it makes no sense to run a db query @@ -195,6 +201,7 @@ abstract class Mapper { * @param array $array * @return bool true if associative * @since 8.1.0 + * @deprecated 14.0.0 Move over to QBMapper */ private function isAssocArray(array $array) { return array_values($array) !== $array; @@ -205,6 +212,7 @@ abstract class Mapper { * @param $value * @return int PDO constant * @since 8.1.0 + * @deprecated 14.0.0 Move over to QBMapper */ private function getPDOType($value) { switch (gettype($value)) { @@ -226,6 +234,7 @@ abstract class Mapper { * @param int $offset from which row we want to start * @return \PDOStatement the database query result * @since 7.0.0 + * @deprecated 14.0.0 Move over to QBMapper */ protected function execute($sql, array $params=[], $limit=null, $offset=null){ $query = $this->db->prepare($sql, $limit, $offset); @@ -249,7 +258,6 @@ abstract class Mapper { return $query; } - /** * Returns an db result and throws exceptions when there are more or less * results @@ -262,6 +270,7 @@ abstract class Mapper { * @throws MultipleObjectsReturnedException if more than one item exist * @return array the result as row * @since 7.0.0 + * @deprecated 14.0.0 Move over to QBMapper */ protected function findOneQuery($sql, array $params=[], $limit=null, $offset=null){ $stmt = $this->execute($sql, $params, $limit, $offset); @@ -297,6 +306,7 @@ abstract class Mapper { * @param int $offset from which row we want to start * @return string formatted error message string * @since 9.1.0 + * @deprecated 14.0.0 Move over to QBMapper */ private function buildDebugMessage($msg, $sql, array $params=[], $limit=null, $offset=null) { return $msg . @@ -313,6 +323,7 @@ abstract class Mapper { * @param array $row the row which should be converted to an entity * @return Entity the entity * @since 7.0.0 + * @deprecated 14.0.0 Move over to QBMapper */ protected function mapRowToEntity($row) { return call_user_func($this->entityClass .'::fromRow', $row); @@ -327,6 +338,7 @@ abstract class Mapper { * @param int $offset from which row we want to start * @return array all fetched entities * @since 7.0.0 + * @deprecated 14.0.0 Move over to QBMapper */ protected function findEntities($sql, array $params=[], $limit=null, $offset=null) { $stmt = $this->execute($sql, $params, $limit, $offset); @@ -354,6 +366,7 @@ abstract class Mapper { * @throws MultipleObjectsReturnedException if more than one item exist * @return Entity the entity * @since 7.0.0 + * @deprecated 14.0.0 Move over to QBMapper */ protected function findEntity($sql, array $params=[], $limit=null, $offset=null){ return $this->mapRowToEntity($this->findOneQuery($sql, $params, $limit, $offset)); diff --git a/lib/public/AppFramework/Db/QBMapper.php b/lib/public/AppFramework/Db/QBMapper.php new file mode 100644 index 0000000000..0eaa7601f2 --- /dev/null +++ b/lib/public/AppFramework/Db/QBMapper.php @@ -0,0 +1,270 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\AppFramework\Db; + +use OCP\DB\QueryBuilder\IQueryBuilder; +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 + */ +abstract class QBMapper { + + /** @var string */ + protected $tableName; + + /** @var string */ + 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 string $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 + * @return Entity the deleted entity + * @since 14.0.0 + */ + public function delete(Entity $entity): Entity { + $qb = $this->db->getQueryBuilder(); + + $qb->delete($this->tableName) + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($entity->getId())) + ); + $qb->execute(); + return $entity; + } + + + /** + * Creates a new entry in the db from an entity + * @param Entity $entity the entity that should be created + * @return Entity the saved entity with the set id + * @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(); + + $qb->setValue($column, $qb->createNamedParameter($value)); + } + + $qb->execute(); + + $entity->setId((int) $qb->getLastInsertId()); + + return $entity; + } + + + + /** + * Updates an entry in the db from an entity + * @throws \InvalidArgumentException if entity has no id + * @param Entity $entity the entity that should be created + * @return Entity the saved entity with the set 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(); + + $qb->set($column, $qb->createNamedParameter($value)); + } + + $qb->where( + $qb->expr()->eq('id', $qb->createNamedParameter($id)) + ); + $qb->execute(); + + return $entity; + } + + /** + * Returns an db result and throws exceptions when there are more or less + * results + * + * @see findEntity + * + * @param IQueryBuilder $query + * @throws DoesNotExistException if the item does not exist + * @throws MultipleObjectsReturnedException if more than one item exist + * @return array the result as row + * @since 14.0.0 + */ + protected function findOneQuery(IQueryBuilder $query): array { + $cursor = $query->execute(); + + $row = $cursor->fetch(); + if($row === false) { + $cursor->closeCursor(); + $msg = $this->buildDebugMessage( + 'Did expect one result but found none when executing', $query + ); + throw new DoesNotExistException($msg); + } + + $row2 = $cursor->fetch(); + $cursor->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 + * @since 14.0.0 + */ + protected function mapRowToEntity(array $row): Entity { + return \call_user_func($this->entityClass .'::fromRow', $row); + } + + + /** + * Runs a sql query and returns an array of entities + * + * @param IQueryBuilder $query + * @return Entity[] all fetched entities + * @since 14.0.0 + */ + protected function findEntities(IQueryBuilder $query): array { + $cursor = $query->execute(); + + $entities = []; + + while($row = $cursor->fetch()){ + $entities[] = $this->mapRowToEntity($row); + } + + $cursor->closeCursor(); + + return $entities; + } + + + /** + * Returns an db result and throws exceptions when there are more or less + * results + * + * @param IQueryBuilder $query + * @throws DoesNotExistException if the item does not exist + * @throws MultipleObjectsReturnedException if more than one item exist + * @return Entity the entity + * @since 14.0.0 + */ + protected function findEntity(IQueryBuilder $query): Entity { + return $this->mapRowToEntity($this->findOneQuery($query)); + } + +}