ManyToManyPersister.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. <?php
  2. /*
  3. * $Id$
  4. *
  5. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  6. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  7. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  8. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  9. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  10. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  11. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  12. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  13. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  14. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  15. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  16. *
  17. * This software consists of voluntary contributions made by many individuals
  18. * and is licensed under the LGPL. For more information, see
  19. * <http://www.doctrine-project.org>.
  20. */
  21. namespace Doctrine\ORM\Persisters;
  22. use Doctrine\ORM\Mapping\ClassMetadata,
  23. Doctrine\ORM\PersistentCollection,
  24. Doctrine\ORM\UnitOfWork;
  25. /**
  26. * Persister for many-to-many collections.
  27. *
  28. * @author Roman Borschel <roman@code-factory.org>
  29. * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
  30. * @author Alexander <iam.asm89@gmail.com>
  31. * @since 2.0
  32. */
  33. class ManyToManyPersister extends AbstractCollectionPersister
  34. {
  35. /**
  36. * {@inheritdoc}
  37. *
  38. * @override
  39. */
  40. protected function _getDeleteRowSQL(PersistentCollection $coll)
  41. {
  42. $mapping = $coll->getMapping();
  43. $class = $this->_em->getClassMetadata(get_class($coll->getOwner()));
  44. return 'DELETE FROM ' . $class->getQuotedJoinTableName($mapping, $this->_conn->getDatabasePlatform())
  45. . ' WHERE ' . implode(' = ? AND ', $mapping['joinTableColumns']) . ' = ?';
  46. }
  47. /**
  48. * {@inheritdoc}
  49. *
  50. * @override
  51. * @internal Order of the parameters must be the same as the order of the columns in
  52. * _getDeleteRowSql.
  53. */
  54. protected function _getDeleteRowSQLParameters(PersistentCollection $coll, $element)
  55. {
  56. return $this->_collectJoinTableColumnParameters($coll, $element);
  57. }
  58. /**
  59. * {@inheritdoc}
  60. *
  61. * @override
  62. */
  63. protected function _getUpdateRowSQL(PersistentCollection $coll)
  64. {}
  65. /**
  66. * {@inheritdoc}
  67. *
  68. * @override
  69. * @internal Order of the parameters must be the same as the order of the columns in
  70. * _getInsertRowSql.
  71. */
  72. protected function _getInsertRowSQL(PersistentCollection $coll)
  73. {
  74. $mapping = $coll->getMapping();
  75. $columns = $mapping['joinTableColumns'];
  76. $class = $this->_em->getClassMetadata(get_class($coll->getOwner()));
  77. $joinTable = $class->getQuotedJoinTableName($mapping, $this->_conn->getDatabasePlatform());
  78. return 'INSERT INTO ' . $joinTable . ' (' . implode(', ', $columns) . ')'
  79. . ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')';
  80. }
  81. /**
  82. * {@inheritdoc}
  83. *
  84. * @override
  85. * @internal Order of the parameters must be the same as the order of the columns in
  86. * _getInsertRowSql.
  87. */
  88. protected function _getInsertRowSQLParameters(PersistentCollection $coll, $element)
  89. {
  90. return $this->_collectJoinTableColumnParameters($coll, $element);
  91. }
  92. /**
  93. * Collects the parameters for inserting/deleting on the join table in the order
  94. * of the join table columns as specified in ManyToManyMapping#joinTableColumns.
  95. *
  96. * @param $coll
  97. * @param $element
  98. * @return array
  99. */
  100. private function _collectJoinTableColumnParameters(PersistentCollection $coll, $element)
  101. {
  102. $params = array();
  103. $mapping = $coll->getMapping();
  104. $isComposite = count($mapping['joinTableColumns']) > 2;
  105. $identifier1 = $this->_uow->getEntityIdentifier($coll->getOwner());
  106. $identifier2 = $this->_uow->getEntityIdentifier($element);
  107. if ($isComposite) {
  108. $class1 = $this->_em->getClassMetadata(get_class($coll->getOwner()));
  109. $class2 = $coll->getTypeClass();
  110. }
  111. foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
  112. $isRelationToSource = isset($mapping['relationToSourceKeyColumns'][$joinTableColumn]);
  113. if ( ! $isComposite) {
  114. $params[] = $isRelationToSource ? array_pop($identifier1) : array_pop($identifier2);
  115. continue;
  116. }
  117. if ($isRelationToSource) {
  118. $params[] = $identifier1[$class1->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])];
  119. continue;
  120. }
  121. $params[] = $identifier2[$class2->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])];
  122. }
  123. return $params;
  124. }
  125. /**
  126. * {@inheritdoc}
  127. *
  128. * @override
  129. */
  130. protected function _getDeleteSQL(PersistentCollection $coll)
  131. {
  132. $class = $this->_em->getClassMetadata(get_class($coll->getOwner()));
  133. $mapping = $coll->getMapping();
  134. return 'DELETE FROM ' . $class->getQuotedJoinTableName($mapping, $this->_conn->getDatabasePlatform())
  135. . ' WHERE ' . implode(' = ? AND ', array_keys($mapping['relationToSourceKeyColumns'])) . ' = ?';
  136. }
  137. /**
  138. * {@inheritdoc}
  139. *
  140. * @override
  141. * @internal Order of the parameters must be the same as the order of the columns in
  142. * _getDeleteSql.
  143. */
  144. protected function _getDeleteSQLParameters(PersistentCollection $coll)
  145. {
  146. $identifier = $this->_uow->getEntityIdentifier($coll->getOwner());
  147. $mapping = $coll->getMapping();
  148. $params = array();
  149. // Optimization for single column identifier
  150. if (count($mapping['relationToSourceKeyColumns']) === 1) {
  151. $params[] = array_pop($identifier);
  152. return $params;
  153. }
  154. // Composite identifier
  155. $sourceClass = $this->_em->getClassMetadata(get_class($mapping->getOwner()));
  156. foreach ($mapping['relationToSourceKeyColumns'] as $relColumn => $srcColumn) {
  157. $params[] = $identifier[$sourceClass->fieldNames[$srcColumn]];
  158. }
  159. return $params;
  160. }
  161. /**
  162. * {@inheritdoc}
  163. */
  164. public function count(PersistentCollection $coll)
  165. {
  166. $mapping = $filterMapping = $coll->getMapping();
  167. $class = $this->_em->getClassMetadata($mapping['sourceEntity']);
  168. $id = $this->_em->getUnitOfWork()->getEntityIdentifier($coll->getOwner());
  169. if ($mapping['isOwningSide']) {
  170. $joinColumns = $mapping['relationToSourceKeyColumns'];
  171. } else {
  172. $mapping = $this->_em->getClassMetadata($mapping['targetEntity'])->associationMappings[$mapping['mappedBy']];
  173. $joinColumns = $mapping['relationToTargetKeyColumns'];
  174. }
  175. $whereClauses = array();
  176. $params = array();
  177. foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
  178. if ( ! isset($joinColumns[$joinTableColumn])) {
  179. continue;
  180. }
  181. $whereClauses[] = $joinTableColumn . ' = ?';
  182. $params[] = ($class->containsForeignIdentifier)
  183. ? $id[$class->getFieldForColumn($joinColumns[$joinTableColumn])]
  184. : $id[$class->fieldNames[$joinColumns[$joinTableColumn]]];
  185. }
  186. list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping);
  187. if ($filterSql) {
  188. $whereClauses[] = $filterSql;
  189. }
  190. $sql = 'SELECT COUNT(*)'
  191. . ' FROM ' . $class->getQuotedJoinTableName($mapping, $this->_conn->getDatabasePlatform()) . ' t'
  192. . $joinTargetEntitySQL
  193. . ' WHERE ' . implode(' AND ', $whereClauses);
  194. return $this->_conn->fetchColumn($sql, $params);
  195. }
  196. /**
  197. * @param PersistentCollection $coll
  198. * @param int $offset
  199. * @param int $length
  200. * @return array
  201. */
  202. public function slice(PersistentCollection $coll, $offset, $length = null)
  203. {
  204. $mapping = $coll->getMapping();
  205. return $this->_em->getUnitOfWork()->getEntityPersister($mapping['targetEntity'])->getManyToManyCollection($mapping, $coll->getOwner(), $offset, $length);
  206. }
  207. /**
  208. * @param PersistentCollection $coll
  209. * @param object $element
  210. * @return boolean
  211. */
  212. public function contains(PersistentCollection $coll, $element)
  213. {
  214. $uow = $this->_em->getUnitOfWork();
  215. // Shortcut for new entities
  216. $entityState = $uow->getEntityState($element, UnitOfWork::STATE_NEW);
  217. if ($entityState === UnitOfWork::STATE_NEW) {
  218. return false;
  219. }
  220. // Entity is scheduled for inclusion
  221. if ($entityState === UnitOfWork::STATE_MANAGED && $uow->isScheduledForInsert($element)) {
  222. return false;
  223. }
  224. list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, $element, true);
  225. $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
  226. return (bool) $this->_conn->fetchColumn($sql, $params);
  227. }
  228. /**
  229. * @param PersistentCollection $coll
  230. * @param object $element
  231. * @return boolean
  232. */
  233. public function removeElement(PersistentCollection $coll, $element)
  234. {
  235. $uow = $this->_em->getUnitOfWork();
  236. // shortcut for new entities
  237. $entityState = $uow->getEntityState($element, UnitOfWork::STATE_NEW);
  238. if ($entityState === UnitOfWork::STATE_NEW) {
  239. return false;
  240. }
  241. // If Entity is scheduled for inclusion, it is not in this collection.
  242. // We can assure that because it would have return true before on array check
  243. if ($entityState === UnitOfWork::STATE_MANAGED && $uow->isScheduledForInsert($element)) {
  244. return false;
  245. }
  246. list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, $element, false);
  247. $sql = 'DELETE FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
  248. return (bool) $this->_conn->executeUpdate($sql, $params);
  249. }
  250. /**
  251. * @param \Doctrine\ORM\PersistentCollection $coll
  252. * @param object $element
  253. * @param boolean $addFilters Whether the filter SQL should be included or not.
  254. * @return array
  255. */
  256. private function getJoinTableRestrictions(PersistentCollection $coll, $element, $addFilters)
  257. {
  258. $uow = $this->_em->getUnitOfWork();
  259. $mapping = $filterMapping = $coll->getMapping();
  260. if ( ! $mapping['isOwningSide']) {
  261. $sourceClass = $this->_em->getClassMetadata($mapping['targetEntity']);
  262. $targetClass = $this->_em->getClassMetadata($mapping['sourceEntity']);
  263. $sourceId = $uow->getEntityIdentifier($element);
  264. $targetId = $uow->getEntityIdentifier($coll->getOwner());
  265. $mapping = $sourceClass->associationMappings[$mapping['mappedBy']];
  266. } else {
  267. $sourceClass = $this->_em->getClassMetadata($mapping['sourceEntity']);
  268. $targetClass = $this->_em->getClassMetadata($mapping['targetEntity']);
  269. $sourceId = $uow->getEntityIdentifier($coll->getOwner());
  270. $targetId = $uow->getEntityIdentifier($element);
  271. }
  272. $quotedJoinTable = $sourceClass->getQuotedJoinTableName($mapping, $this->_conn->getDatabasePlatform());
  273. $whereClauses = array();
  274. $params = array();
  275. foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
  276. $whereClauses[] = $joinTableColumn . ' = ?';
  277. if (isset($mapping['relationToTargetKeyColumns'][$joinTableColumn])) {
  278. $params[] = ($targetClass->containsForeignIdentifier)
  279. ? $targetId[$targetClass->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])]
  280. : $targetId[$targetClass->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]];
  281. continue;
  282. }
  283. // relationToSourceKeyColumns
  284. $params[] = ($sourceClass->containsForeignIdentifier)
  285. ? $sourceId[$sourceClass->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])]
  286. : $sourceId[$sourceClass->fieldNames[$mapping['relationToSourceKeyColumns'][$joinTableColumn]]];
  287. }
  288. if ($addFilters) {
  289. list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping);
  290. if ($filterSql) {
  291. $quotedJoinTable .= ' t ' . $joinTargetEntitySQL;
  292. $whereClauses[] = $filterSql;
  293. }
  294. }
  295. return array($quotedJoinTable, $whereClauses, $params);
  296. }
  297. /**
  298. * Generates the filter SQL for a given mapping.
  299. *
  300. * This method is not used for actually grabbing the related entities
  301. * but when the extra-lazy collection methods are called on a filtered
  302. * association. This is why besides the many to many table we also
  303. * have to join in the actual entities table leading to additional
  304. * JOIN.
  305. *
  306. * @param array $mapping Array containing mapping information.
  307. *
  308. * @return string The SQL query part to add to a query.
  309. */
  310. public function getFilterSql($mapping)
  311. {
  312. $targetClass = $this->_em->getClassMetadata($mapping['targetEntity']);
  313. if ($mapping['isOwningSide']) {
  314. $joinColumns = $mapping['relationToTargetKeyColumns'];
  315. } else {
  316. $mapping = $targetClass->associationMappings[$mapping['mappedBy']];
  317. $joinColumns = $mapping['relationToSourceKeyColumns'];
  318. }
  319. $targetClass = $this->_em->getClassMetadata($targetClass->rootEntityName);
  320. // A join is needed if there is filtering on the target entity
  321. $joinTargetEntitySQL = '';
  322. if ($filterSql = $this->generateFilterConditionSQL($targetClass, 'te')) {
  323. $joinTargetEntitySQL = ' JOIN '
  324. . $targetClass->getQuotedTableName($this->_conn->getDatabasePlatform()) . ' te'
  325. . ' ON';
  326. $joinTargetEntitySQLClauses = array();
  327. foreach ($joinColumns as $joinTableColumn => $targetTableColumn) {
  328. $joinTargetEntitySQLClauses[] = ' t.' . $joinTableColumn . ' = ' . 'te.' . $targetTableColumn;
  329. }
  330. $joinTargetEntitySQL .= implode(' AND ', $joinTargetEntitySQLClauses);
  331. }
  332. return array($joinTargetEntitySQL, $filterSql);
  333. }
  334. /**
  335. * Generates the filter SQL for a given entity and table alias.
  336. *
  337. * @param ClassMetadata $targetEntity Metadata of the target entity.
  338. * @param string $targetTableAlias The table alias of the joined/selected table.
  339. *
  340. * @return string The SQL query part to add to a query.
  341. */
  342. protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
  343. {
  344. $filterClauses = array();
  345. foreach ($this->_em->getFilters()->getEnabledFilters() as $filter) {
  346. if ($filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
  347. $filterClauses[] = '(' . $filterExpr . ')';
  348. }
  349. }
  350. $sql = implode(' AND ', $filterClauses);
  351. return $sql ? "(" . $sql . ")" : "";
  352. }
  353. }