QBMapper.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCP\AppFramework\Db;
  8. use Generator;
  9. use OCP\DB\Exception;
  10. use OCP\DB\QueryBuilder\IQueryBuilder;
  11. use OCP\IDBConnection;
  12. /**
  13. * Simple parent class for inheriting your data access layer from. This class
  14. * may be subject to change in the future
  15. *
  16. * @since 14.0.0
  17. *
  18. * @template T of Entity
  19. */
  20. abstract class QBMapper {
  21. /** @var string */
  22. protected $tableName;
  23. /** @var string|class-string<T> */
  24. protected $entityClass;
  25. /** @var IDBConnection */
  26. protected $db;
  27. /**
  28. * @param IDBConnection $db Instance of the Db abstraction layer
  29. * @param string $tableName the name of the table. set this to allow entity
  30. * @param class-string<T>|null $entityClass the name of the entity that the sql should be
  31. * mapped to queries without using sql
  32. * @since 14.0.0
  33. */
  34. public function __construct(IDBConnection $db, string $tableName, ?string $entityClass = null) {
  35. $this->db = $db;
  36. $this->tableName = $tableName;
  37. // if not given set the entity name to the class without the mapper part
  38. // cache it here for later use since reflection is slow
  39. if ($entityClass === null) {
  40. $this->entityClass = str_replace('Mapper', '', \get_class($this));
  41. } else {
  42. $this->entityClass = $entityClass;
  43. }
  44. }
  45. /**
  46. * @return string the table name
  47. * @since 14.0.0
  48. */
  49. public function getTableName(): string {
  50. return $this->tableName;
  51. }
  52. /**
  53. * Deletes an entity from the table
  54. *
  55. * @param Entity $entity the entity that should be deleted
  56. * @psalm-param T $entity the entity that should be deleted
  57. * @return Entity the deleted entity
  58. * @psalm-return T the deleted entity
  59. * @throws Exception
  60. * @since 14.0.0
  61. */
  62. public function delete(Entity $entity): Entity {
  63. $qb = $this->db->getQueryBuilder();
  64. $idType = $this->getParameterTypeForProperty($entity, 'id');
  65. $qb->delete($this->tableName)
  66. ->where(
  67. $qb->expr()->eq('id', $qb->createNamedParameter($entity->getId(), $idType))
  68. );
  69. $qb->executeStatement();
  70. return $entity;
  71. }
  72. /**
  73. * Creates a new entry in the db from an entity
  74. *
  75. * @param Entity $entity the entity that should be created
  76. * @psalm-param T $entity the entity that should be created
  77. * @return Entity the saved entity with the set id
  78. * @psalm-return T the saved entity with the set id
  79. * @throws Exception
  80. * @since 14.0.0
  81. */
  82. public function insert(Entity $entity): Entity {
  83. // get updated fields to save, fields have to be set using a setter to
  84. // be saved
  85. $properties = $entity->getUpdatedFields();
  86. $qb = $this->db->getQueryBuilder();
  87. $qb->insert($this->tableName);
  88. // build the fields
  89. foreach ($properties as $property => $updated) {
  90. $column = $entity->propertyToColumn($property);
  91. $getter = 'get' . ucfirst($property);
  92. $value = $entity->$getter();
  93. $type = $this->getParameterTypeForProperty($entity, $property);
  94. $qb->setValue($column, $qb->createNamedParameter($value, $type));
  95. }
  96. $qb->executeStatement();
  97. if ($entity->id === null) {
  98. // When autoincrement is used id is always an int
  99. $entity->setId($qb->getLastInsertId());
  100. }
  101. return $entity;
  102. }
  103. /**
  104. * Tries to creates a new entry in the db from an entity and
  105. * updates an existing entry if duplicate keys are detected
  106. * by the database
  107. *
  108. * @param Entity $entity the entity that should be created/updated
  109. * @psalm-param T $entity the entity that should be created/updated
  110. * @return Entity the saved entity with the (new) id
  111. * @psalm-return T the saved entity with the (new) id
  112. * @throws Exception
  113. * @throws \InvalidArgumentException if entity has no id
  114. * @since 15.0.0
  115. */
  116. public function insertOrUpdate(Entity $entity): Entity {
  117. try {
  118. return $this->insert($entity);
  119. } catch (Exception $ex) {
  120. if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
  121. return $this->update($entity);
  122. }
  123. throw $ex;
  124. }
  125. }
  126. /**
  127. * Updates an entry in the db from an entity
  128. *
  129. * @param Entity $entity the entity that should be created
  130. * @psalm-param T $entity the entity that should be created
  131. * @return Entity the saved entity with the set id
  132. * @psalm-return T the saved entity with the set id
  133. * @throws Exception
  134. * @throws \InvalidArgumentException if entity has no id
  135. * @since 14.0.0
  136. */
  137. public function update(Entity $entity): Entity {
  138. // if entity wasn't changed it makes no sense to run a db query
  139. $properties = $entity->getUpdatedFields();
  140. if (\count($properties) === 0) {
  141. return $entity;
  142. }
  143. // entity needs an id
  144. $id = $entity->getId();
  145. if ($id === null) {
  146. throw new \InvalidArgumentException(
  147. 'Entity which should be updated has no id');
  148. }
  149. // get updated fields to save, fields have to be set using a setter to
  150. // be saved
  151. // do not update the id field
  152. unset($properties['id']);
  153. $qb = $this->db->getQueryBuilder();
  154. $qb->update($this->tableName);
  155. // build the fields
  156. foreach ($properties as $property => $updated) {
  157. $column = $entity->propertyToColumn($property);
  158. $getter = 'get' . ucfirst($property);
  159. $value = $entity->$getter();
  160. $type = $this->getParameterTypeForProperty($entity, $property);
  161. $qb->set($column, $qb->createNamedParameter($value, $type));
  162. }
  163. $idType = $this->getParameterTypeForProperty($entity, 'id');
  164. $qb->where(
  165. $qb->expr()->eq('id', $qb->createNamedParameter($id, $idType))
  166. );
  167. $qb->executeStatement();
  168. return $entity;
  169. }
  170. /**
  171. * Returns the type parameter for the QueryBuilder for a specific property
  172. * of the $entity
  173. *
  174. * @param Entity $entity The entity to get the types from
  175. * @psalm-param T $entity
  176. * @param string $property The property of $entity to get the type for
  177. * @return int|string
  178. * @since 16.0.0
  179. */
  180. protected function getParameterTypeForProperty(Entity $entity, string $property) {
  181. $types = $entity->getFieldTypes();
  182. if (!isset($types[ $property ])) {
  183. return IQueryBuilder::PARAM_STR;
  184. }
  185. switch ($types[ $property ]) {
  186. case 'int':
  187. case 'integer':
  188. return IQueryBuilder::PARAM_INT;
  189. case 'string':
  190. return IQueryBuilder::PARAM_STR;
  191. case 'bool':
  192. case 'boolean':
  193. return IQueryBuilder::PARAM_BOOL;
  194. case 'blob':
  195. return IQueryBuilder::PARAM_LOB;
  196. case 'datetime':
  197. return IQueryBuilder::PARAM_DATE;
  198. case 'json':
  199. return IQueryBuilder::PARAM_JSON;
  200. }
  201. return IQueryBuilder::PARAM_STR;
  202. }
  203. /**
  204. * Returns an db result and throws exceptions when there are more or less
  205. * results
  206. *
  207. * @param IQueryBuilder $query
  208. * @return array the result as row
  209. * @throws Exception
  210. * @throws MultipleObjectsReturnedException if more than one item exist
  211. * @throws DoesNotExistException if the item does not exist
  212. * @see findEntity
  213. *
  214. * @since 14.0.0
  215. */
  216. protected function findOneQuery(IQueryBuilder $query): array {
  217. $result = $query->executeQuery();
  218. $row = $result->fetch();
  219. if ($row === false) {
  220. $result->closeCursor();
  221. $msg = $this->buildDebugMessage(
  222. 'Did expect one result but found none when executing', $query
  223. );
  224. throw new DoesNotExistException($msg);
  225. }
  226. $row2 = $result->fetch();
  227. $result->closeCursor();
  228. if ($row2 !== false) {
  229. $msg = $this->buildDebugMessage(
  230. 'Did not expect more than one result when executing', $query
  231. );
  232. throw new MultipleObjectsReturnedException($msg);
  233. }
  234. return $row;
  235. }
  236. /**
  237. * @param string $msg
  238. * @param IQueryBuilder $sql
  239. * @return string
  240. * @since 14.0.0
  241. */
  242. private function buildDebugMessage(string $msg, IQueryBuilder $sql): string {
  243. return $msg .
  244. ': query "' . $sql->getSQL() . '"; ';
  245. }
  246. /**
  247. * Creates an entity from a row. Automatically determines the entity class
  248. * from the current mapper name (MyEntityMapper -> MyEntity)
  249. *
  250. * @param array $row the row which should be converted to an entity
  251. * @return Entity the entity
  252. * @psalm-return T the entity
  253. * @since 14.0.0
  254. */
  255. protected function mapRowToEntity(array $row): Entity {
  256. unset($row['DOCTRINE_ROWNUM']); // remove doctrine/dbal helper column
  257. return \call_user_func($this->entityClass .'::fromRow', $row);
  258. }
  259. /**
  260. * Runs a sql query and returns an array of entities
  261. *
  262. * @param IQueryBuilder $query
  263. * @return Entity[] all fetched entities
  264. * @psalm-return T[] all fetched entities
  265. * @throws Exception
  266. * @since 14.0.0
  267. */
  268. protected function findEntities(IQueryBuilder $query): array {
  269. $result = $query->executeQuery();
  270. try {
  271. $entities = [];
  272. while ($row = $result->fetch()) {
  273. $entities[] = $this->mapRowToEntity($row);
  274. }
  275. return $entities;
  276. } finally {
  277. $result->closeCursor();
  278. }
  279. }
  280. /**
  281. * Runs a sql query and yields each resulting entity to obtain database entries in a memory-efficient way
  282. *
  283. * @param IQueryBuilder $query
  284. * @return Generator Generator of fetched entities
  285. * @psalm-return Generator<T> Generator of fetched entities
  286. * @throws Exception
  287. * @since 30.0.0
  288. */
  289. protected function yieldEntities(IQueryBuilder $query): Generator {
  290. $result = $query->executeQuery();
  291. try {
  292. while ($row = $result->fetch()) {
  293. yield $this->mapRowToEntity($row);
  294. }
  295. } finally {
  296. $result->closeCursor();
  297. }
  298. }
  299. /**
  300. * Returns an db result and throws exceptions when there are more or less
  301. * results
  302. *
  303. * @param IQueryBuilder $query
  304. * @return Entity the entity
  305. * @psalm-return T the entity
  306. * @throws Exception
  307. * @throws MultipleObjectsReturnedException if more than one item exist
  308. * @throws DoesNotExistException if the item does not exist
  309. * @since 14.0.0
  310. */
  311. protected function findEntity(IQueryBuilder $query): Entity {
  312. return $this->mapRowToEntity($this->findOneQuery($query));
  313. }
  314. }