CompositeKeysTest.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Test\TestCase\ORM;
  16. use Cake\Datasource\ConnectionManager;
  17. use Cake\ORM\Entity;
  18. use Cake\ORM\Marshaller;
  19. use Cake\ORM\Query;
  20. use Cake\ORM\Table;
  21. use Cake\ORM\TableRegistry;
  22. use Cake\TestSuite\TestCase;
  23. /**
  24. * Test entity for mass assignment.
  25. */
  26. class OpenArticleEntity extends Entity
  27. {
  28. protected $_accessible = [
  29. '*' => true
  30. ];
  31. }
  32. /**
  33. * Integration tetss for table operations involving composite keys
  34. */
  35. class CompositeKeyTest extends TestCase
  36. {
  37. /**
  38. * Fixture to be used
  39. *
  40. * @var array
  41. */
  42. public $fixtures = [
  43. 'core.site_articles',
  44. 'core.site_authors',
  45. 'core.site_tags',
  46. 'core.site_articles_tags'
  47. ];
  48. /**
  49. * setUp method
  50. *
  51. * @return void
  52. */
  53. public function setUp()
  54. {
  55. parent::setUp();
  56. $this->connection = ConnectionManager::get('test');
  57. }
  58. /**
  59. * Data provider for the two types of strategies HasMany implements
  60. *
  61. * @return void
  62. */
  63. public function strategiesProvider()
  64. {
  65. return [['subquery'], ['select']];
  66. }
  67. /**
  68. * Tests that HasMany associations are correctly eager loaded and results
  69. * correctly nested when multiple foreignKeys are used
  70. *
  71. * @dataProvider strategiesProvider
  72. * @return void
  73. */
  74. public function testHasManyEager($strategy)
  75. {
  76. $table = TableRegistry::get('SiteAuthors');
  77. $table->hasMany('SiteArticles', [
  78. 'propertyName' => 'articles',
  79. 'strategy' => $strategy,
  80. 'sort' => ['SiteArticles.id' => 'asc'],
  81. 'foreignKey' => ['author_id', 'site_id']
  82. ]);
  83. $query = new Query($this->connection, $table);
  84. $results = $query->select()
  85. ->contain('SiteArticles')
  86. ->hydrate(false)
  87. ->toArray();
  88. $expected = [
  89. [
  90. 'id' => 1,
  91. 'name' => 'mark',
  92. 'site_id' => 1,
  93. 'articles' => [
  94. [
  95. 'id' => 1,
  96. 'title' => 'First Article',
  97. 'body' => 'First Article Body',
  98. 'author_id' => 1,
  99. 'site_id' => 1
  100. ]
  101. ]
  102. ],
  103. [
  104. 'id' => 2,
  105. 'name' => 'juan',
  106. 'site_id' => 2,
  107. 'articles' => [],
  108. ],
  109. [
  110. 'id' => 3,
  111. 'name' => 'jose',
  112. 'site_id' => 2,
  113. 'articles' => [
  114. [
  115. 'id' => 2,
  116. 'title' => 'Second Article',
  117. 'body' => 'Second Article Body',
  118. 'author_id' => 3,
  119. 'site_id' => 2
  120. ]
  121. ]
  122. ],
  123. [
  124. 'id' => 4,
  125. 'name' => 'andy',
  126. 'site_id' => 1,
  127. 'articles' => [],
  128. ]
  129. ];
  130. $this->assertEquals($expected, $results);
  131. $results = $query->repository($table)
  132. ->select()
  133. ->contain(['SiteArticles' => ['conditions' => ['SiteArticles.id' => 2]]])
  134. ->hydrate(false)
  135. ->toArray();
  136. $expected[0]['articles'] = [];
  137. $this->assertEquals($expected, $results);
  138. $this->assertEquals($table->association('SiteArticles')->strategy(), $strategy);
  139. }
  140. /**
  141. * Tests that BelongsToMany associations are correctly eager loaded when multiple
  142. * foreignKeys are used
  143. *
  144. * @dataProvider strategiesProvider
  145. * @return void
  146. */
  147. public function testBelongsToManyEager($strategy)
  148. {
  149. $articles = TableRegistry::get('SiteArticles');
  150. $tags = TableRegistry::get('SiteTags');
  151. $junction = TableRegistry::get('SiteArticlesTags');
  152. $articles->belongsToMany('SiteTags', [
  153. 'strategy' => $strategy,
  154. 'targetTable' => $tags,
  155. 'propertyName' => 'tags',
  156. 'through' => 'SiteArticlesTags',
  157. 'foreignKey' => ['article_id', 'site_id'],
  158. 'targetForeignKey' => ['tag_id', 'site_id']
  159. ]);
  160. $query = new Query($this->connection, $articles);
  161. $results = $query->select()->contain('SiteTags')->hydrate(false)->toArray();
  162. $expected = [
  163. [
  164. 'id' => 1,
  165. 'author_id' => 1,
  166. 'title' => 'First Article',
  167. 'body' => 'First Article Body',
  168. 'site_id' => 1,
  169. 'tags' => [
  170. [
  171. 'id' => 1,
  172. 'name' => 'tag1',
  173. '_joinData' => ['article_id' => 1, 'tag_id' => 1, 'site_id' => 1],
  174. 'site_id' => 1
  175. ],
  176. [
  177. 'id' => 3,
  178. 'name' => 'tag3',
  179. '_joinData' => ['article_id' => 1, 'tag_id' => 3, 'site_id' => 1],
  180. 'site_id' => 1
  181. ]
  182. ]
  183. ],
  184. [
  185. 'id' => 2,
  186. 'title' => 'Second Article',
  187. 'body' => 'Second Article Body',
  188. 'author_id' => 3,
  189. 'site_id' => 2,
  190. 'tags' => [
  191. [
  192. 'id' => 4,
  193. 'name' => 'tag4',
  194. '_joinData' => ['article_id' => 2, 'tag_id' => 4, 'site_id' => 2],
  195. 'site_id' => 2
  196. ]
  197. ]
  198. ],
  199. [
  200. 'id' => 3,
  201. 'title' => 'Third Article',
  202. 'body' => 'Third Article Body',
  203. 'author_id' => 1,
  204. 'site_id' => 2,
  205. 'tags' => [],
  206. ],
  207. [
  208. 'id' => 4,
  209. 'title' => 'Fourth Article',
  210. 'body' => 'Fourth Article Body',
  211. 'author_id' => 3,
  212. 'site_id' => 1,
  213. 'tags' => [
  214. [
  215. 'id' => 1,
  216. 'name' => 'tag1',
  217. '_joinData' => ['article_id' => 4, 'tag_id' => 1, 'site_id' => 1],
  218. 'site_id' => 1
  219. ]
  220. ]
  221. ],
  222. ];
  223. $this->assertEquals($expected, $results);
  224. $this->assertEquals($articles->association('SiteTags')->strategy(), $strategy);
  225. }
  226. /**
  227. * Provides strategies for associations that can be joined
  228. *
  229. * @return void
  230. */
  231. public function internalStategiesProvider()
  232. {
  233. return [['join'], ['select'], ['subquery']];
  234. }
  235. /**
  236. * Tests loding belongsTo with composite keys
  237. *
  238. * @dataProvider internalStategiesProvider
  239. * @return void
  240. */
  241. public function testBelongsToEager($strategy)
  242. {
  243. $table = TableRegistry::get('SiteArticles');
  244. $table->belongsTo('SiteAuthors', [
  245. 'propertyName' => 'author',
  246. 'strategy' => $strategy,
  247. 'foreignKey' => ['author_id', 'site_id']
  248. ]);
  249. $query = new Query($this->connection, $table);
  250. $results = $query->select()
  251. ->where(['SiteArticles.id IN' => [1, 2]])
  252. ->contain('SiteAuthors')
  253. ->hydrate(false)
  254. ->toArray();
  255. $expected = [
  256. [
  257. 'id' => 1,
  258. 'author_id' => 1,
  259. 'site_id' => 1,
  260. 'title' => 'First Article',
  261. 'body' => 'First Article Body',
  262. 'author' => [
  263. 'id' => 1,
  264. 'name' => 'mark',
  265. 'site_id' => 1
  266. ]
  267. ],
  268. [
  269. 'id' => 2,
  270. 'author_id' => 3,
  271. 'site_id' => 2,
  272. 'title' => 'Second Article',
  273. 'body' => 'Second Article Body',
  274. 'author' => [
  275. 'id' => 3,
  276. 'name' => 'jose',
  277. 'site_id' => 2
  278. ]
  279. ]
  280. ];
  281. $this->assertEquals($expected, $results);
  282. }
  283. /**
  284. * Tests loding hasOne with composite keys
  285. *
  286. * @dataProvider internalStategiesProvider
  287. * @return void
  288. */
  289. public function testHasOneEager($strategy)
  290. {
  291. $table = TableRegistry::get('SiteAuthors');
  292. $table->hasOne('SiteArticles', [
  293. 'propertyName' => 'first_article',
  294. 'strategy' => $strategy,
  295. 'foreignKey' => ['author_id', 'site_id']
  296. ]);
  297. $query = new Query($this->connection, $table);
  298. $results = $query->select()
  299. ->where(['SiteAuthors.id IN' => [1, 3]])
  300. ->contain('SiteArticles')
  301. ->hydrate(false)
  302. ->toArray();
  303. $expected = [
  304. [
  305. 'id' => 1,
  306. 'name' => 'mark',
  307. 'site_id' => 1,
  308. 'first_article' => [
  309. 'id' => 1,
  310. 'author_id' => 1,
  311. 'site_id' => 1,
  312. 'title' => 'First Article',
  313. 'body' => 'First Article Body'
  314. ]
  315. ],
  316. [
  317. 'id' => 3,
  318. 'name' => 'jose',
  319. 'site_id' => 2,
  320. 'first_article' => [
  321. 'id' => 2,
  322. 'author_id' => 3,
  323. 'site_id' => 2,
  324. 'title' => 'Second Article',
  325. 'body' => 'Second Article Body'
  326. ]
  327. ]
  328. ];
  329. $this->assertEquals($expected, $results);
  330. }
  331. /**
  332. * Tests that it is possible to insert a new row using the save method
  333. * if the entity has composite primary key
  334. *
  335. * @group save
  336. * @return void
  337. */
  338. public function testSaveNewEntity()
  339. {
  340. $entity = new \Cake\ORM\Entity([
  341. 'id' => 5,
  342. 'site_id' => 1,
  343. 'title' => 'Fifth Article',
  344. 'body' => 'Fifth Article Body',
  345. 'author_id' => 3,
  346. ]);
  347. $table = TableRegistry::get('SiteArticles');
  348. $this->assertSame($entity, $table->save($entity));
  349. $this->assertEquals($entity->id, 5);
  350. $row = $table->find('all')->where(['id' => 5, 'site_id' => 1])->first();
  351. $this->assertEquals($entity->toArray(), $row->toArray());
  352. }
  353. /**
  354. * Tests that it is possible to insert a new row using the save method
  355. * if the entity has composite primary key
  356. *
  357. * @group save
  358. * @expectedException \RuntimeException
  359. * @expectedExceptionMessage Cannot insert row, some of the primary key values are missing. Got (5, ), expecting (id, site_id)
  360. * @return void
  361. */
  362. public function testSaveNewEntityMissingKey()
  363. {
  364. $entity = new \Cake\ORM\Entity([
  365. 'id' => 5,
  366. 'title' => 'Fifth Article',
  367. 'body' => 'Fifth Article Body',
  368. 'author_id' => 3,
  369. ]);
  370. $table = TableRegistry::get('SiteArticles');
  371. $table->save($entity);
  372. }
  373. /**
  374. * Test simple delete with composite primary key
  375. *
  376. * @return void
  377. */
  378. public function testDelete()
  379. {
  380. $table = TableRegistry::get('SiteAuthors');
  381. $table->save(new \Cake\ORM\Entity(['id' => 1, 'site_id' => 2]));
  382. $entity = $table->get([1, 1]);
  383. $result = $table->delete($entity);
  384. $this->assertTrue($result);
  385. $this->assertEquals(4, $table->find('all')->count());
  386. $this->assertEmpty($table->find()->where(['id' => 1, 'site_id' => 1])->first());
  387. }
  388. /**
  389. * Test delete with dependent records having composite keys
  390. *
  391. * @return void
  392. */
  393. public function testDeleteDependent()
  394. {
  395. $table = TableRegistry::get('SiteAuthors');
  396. $table->hasMany('SiteArticles', [
  397. 'foreignKey' => ['author_id', 'site_id'],
  398. 'dependent' => true,
  399. ]);
  400. $entity = $table->get([3, 2]);
  401. $result = $table->delete($entity);
  402. $query = $table->association('SiteArticles')->find('all', [
  403. 'conditions' => [
  404. 'author_id' => $entity->id,
  405. 'site_id' => $entity->site_id
  406. ]
  407. ]);
  408. $this->assertNull($query->all()->first(), 'Should not find any rows.');
  409. }
  410. /**
  411. * Test generating a list of entities from a list of composite ids
  412. *
  413. * @return void
  414. */
  415. public function testOneGenerateBelongsToManyEntitiesFromIds()
  416. {
  417. $articles = TableRegistry::get('SiteArticles');
  418. $articles->entityClass(__NAMESPACE__ . '\OpenArticleEntity');
  419. $tags = TableRegistry::get('SiteTags');
  420. $junction = TableRegistry::get('SiteArticlesTags');
  421. $articles->belongsToMany('SiteTags', [
  422. 'targetTable' => $tags,
  423. 'propertyName' => 'tags',
  424. 'through' => 'SiteArticlesTags',
  425. 'foreignKey' => ['article_id', 'site_id'],
  426. 'targetForeignKey' => ['tag_id', 'site_id']
  427. ]);
  428. $data = [
  429. 'title' => 'Haz tags',
  430. 'body' => 'Some content here',
  431. 'tags' => ['_ids' => [[1, 1], [2, 2], [3, 1]]]
  432. ];
  433. $marshall = new Marshaller($articles);
  434. $result = $marshall->one($data, ['associated' => ['SiteTags']]);
  435. $this->assertCount(3, $result->tags);
  436. $this->assertInstanceOf('Cake\ORM\Entity', $result->tags[0]);
  437. $this->assertInstanceOf('Cake\ORM\Entity', $result->tags[1]);
  438. $this->assertInstanceOf('Cake\ORM\Entity', $result->tags[2]);
  439. $data = [
  440. 'title' => 'Haz tags',
  441. 'body' => 'Some content here',
  442. 'tags' => ['_ids' => [1, 2, 3]]
  443. ];
  444. $marshall = new Marshaller($articles);
  445. $result = $marshall->one($data, ['associated' => ['SiteTags']]);
  446. $this->assertEmpty($result->tags);
  447. }
  448. /**
  449. * Tests find('list') with composite keys
  450. *
  451. * @return void
  452. */
  453. public function testFindListCompositeKeys()
  454. {
  455. $table = new Table([
  456. 'table' => 'site_authors',
  457. 'connection' => $this->connection,
  458. ]);
  459. $table->displayField('name');
  460. $query = $table->find('list')
  461. ->hydrate(false)
  462. ->order('id');
  463. $expected = [
  464. '1;1' => 'mark',
  465. '2;2' => 'juan',
  466. '3;2' => 'jose',
  467. '4;1' => 'andy'
  468. ];
  469. $this->assertEquals($expected, $query->toArray());
  470. $table->displayField(['name', 'site_id']);
  471. $query = $table->find('list')
  472. ->hydrate(false)
  473. ->order('id');
  474. $expected = [
  475. '1;1' => 'mark;1',
  476. '2;2' => 'juan;2',
  477. '3;2' => 'jose;2',
  478. '4;1' => 'andy;1'
  479. ];
  480. $this->assertEquals($expected, $query->toArray());
  481. $query = $table->find('list', ['groupField' => ['site_id', 'site_id']])
  482. ->hydrate(false)
  483. ->order('id');
  484. $expected = [
  485. '1;1' => [
  486. '1;1' => 'mark;1',
  487. '4;1' => 'andy;1'
  488. ],
  489. '2;2' => [
  490. '2;2' => 'juan;2',
  491. '3;2' => 'jose;2'
  492. ]
  493. ];
  494. $this->assertEquals($expected, $query->toArray());
  495. }
  496. /**
  497. * Tests find('threaded') with composite keys
  498. *
  499. * @return void
  500. */
  501. public function testFindThreadedCompositeKeys()
  502. {
  503. $table = TableRegistry::get('SiteAuthors');
  504. $query = $this->getMock(
  505. '\Cake\ORM\Query',
  506. ['_addDefaultFields', 'execute'],
  507. [null, $table]
  508. );
  509. $items = new \Cake\Datasource\ResultSetDecorator([
  510. ['id' => 1, 'name' => 'a', 'site_id' => 1, 'parent_id' => null],
  511. ['id' => 2, 'name' => 'a', 'site_id' => 2, 'parent_id' => null],
  512. ['id' => 3, 'name' => 'a', 'site_id' => 1, 'parent_id' => 1],
  513. ['id' => 4, 'name' => 'a', 'site_id' => 2, 'parent_id' => 2],
  514. ['id' => 5, 'name' => 'a', 'site_id' => 2, 'parent_id' => 4],
  515. ['id' => 6, 'name' => 'a', 'site_id' => 1, 'parent_id' => 2],
  516. ['id' => 7, 'name' => 'a', 'site_id' => 1, 'parent_id' => 3],
  517. ['id' => 8, 'name' => 'a', 'site_id' => 2, 'parent_id' => 4],
  518. ]);
  519. $query->find('threaded', ['parentField' => ['parent_id', 'site_id']]);
  520. $formatter = $query->formatResults()[0];
  521. $expected = [
  522. [
  523. 'id' => 1,
  524. 'name' => 'a',
  525. 'site_id' => 1,
  526. 'parent_id' => null,
  527. 'children' => [
  528. [
  529. 'id' => 3,
  530. 'name' => 'a',
  531. 'site_id' => 1,
  532. 'parent_id' => 1,
  533. 'children' => [
  534. [
  535. 'id' => 7,
  536. 'name' => 'a',
  537. 'site_id' => 1,
  538. 'parent_id' => 3,
  539. 'children' => []
  540. ]
  541. ]
  542. ]
  543. ]
  544. ],
  545. [
  546. 'id' => 2,
  547. 'name' => 'a',
  548. 'site_id' => 2,
  549. 'parent_id' => null,
  550. 'children' => [
  551. [
  552. 'id' => 4,
  553. 'name' => 'a',
  554. 'site_id' => 2,
  555. 'parent_id' => 2,
  556. 'children' => [
  557. [
  558. 'id' => 5,
  559. 'name' => 'a',
  560. 'site_id' => 2,
  561. 'parent_id' => 4,
  562. 'children' => []
  563. ],
  564. [
  565. 'id' => 8,
  566. 'name' => 'a',
  567. 'site_id' => 2,
  568. 'parent_id' => 4,
  569. 'children' => []
  570. ]
  571. ]
  572. ]
  573. ]
  574. ],
  575. [
  576. 'id' => 6,
  577. 'name' => 'a',
  578. 'site_id' => 1,
  579. 'parent_id' => 2,
  580. 'children' => []
  581. ]
  582. ];
  583. $this->assertEquals($expected, $formatter($items)->toArray());
  584. }
  585. }