CompositeKeysTest.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  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.composite_increments',
  44. 'core.site_articles',
  45. 'core.site_authors',
  46. 'core.site_tags',
  47. 'core.site_articles_tags'
  48. ];
  49. /**
  50. * setUp method
  51. *
  52. * @return void
  53. */
  54. public function setUp()
  55. {
  56. parent::setUp();
  57. $this->connection = ConnectionManager::get('test');
  58. }
  59. /**
  60. * Data provider for the two types of strategies HasOne implements
  61. *
  62. * @return void
  63. */
  64. public function strategiesProviderHasOne()
  65. {
  66. return [['join'], ['select']];
  67. }
  68. /**
  69. * Data provider for the two types of strategies HasMany implements
  70. *
  71. * @return void
  72. */
  73. public function strategiesProviderHasMany()
  74. {
  75. return [['subquery'], ['select']];
  76. }
  77. /**
  78. * Data provider for the two types of strategies BelongsTo implements
  79. *
  80. * @return void
  81. */
  82. public function strategiesProviderBelongsTo()
  83. {
  84. return [['join'], ['select']];
  85. }
  86. /**
  87. * Data provider for the two types of strategies BelongsToMany implements
  88. *
  89. * @return void
  90. */
  91. public function strategiesProviderBelongsToMany()
  92. {
  93. return [['subquery'], ['select']];
  94. }
  95. /**
  96. * Test that you cannot save rows with composite keys if some columns are missing.
  97. *
  98. * @group save
  99. * @expectedException \RuntimeException
  100. * @expectedExceptionMessage Cannot insert row, some of the primary key values are missing
  101. * @return void
  102. */
  103. public function testSaveNewErrorCompositeKeyNoIncrement()
  104. {
  105. $articles = TableRegistry::get('SiteArticles');
  106. $article = $articles->newEntity(['site_id' => 1, 'author_id' => 1, 'title' => 'testing']);
  107. $articles->save($article);
  108. }
  109. /**
  110. * Test that saving into composite primary keys where one column is missing & autoIncrement works.
  111. *
  112. * SQLite is skipped because it doesn't support autoincrement composite keys.
  113. *
  114. * @group save
  115. * @return void
  116. */
  117. public function testSaveNewCompositeKeyIncrement()
  118. {
  119. $this->skipIfSqlite();
  120. $table = TableRegistry::get('CompositeIncrements');
  121. $thing = $table->newEntity(['account_id' => 3, 'name' => 'new guy']);
  122. $this->assertSame($thing, $table->save($thing));
  123. $this->assertNotEmpty($thing->id, 'Primary key should have been populated');
  124. $this->assertSame(3, $thing->account_id);
  125. }
  126. /**
  127. * Tests that HasMany associations are correctly eager loaded and results
  128. * correctly nested when multiple foreignKeys are used
  129. *
  130. * @dataProvider strategiesProviderHasMany
  131. * @return void
  132. */
  133. public function testHasManyEager($strategy)
  134. {
  135. $table = TableRegistry::get('SiteAuthors');
  136. $table->hasMany('SiteArticles', [
  137. 'propertyName' => 'articles',
  138. 'strategy' => $strategy,
  139. 'sort' => ['SiteArticles.id' => 'asc'],
  140. 'foreignKey' => ['author_id', 'site_id']
  141. ]);
  142. $query = new Query($this->connection, $table);
  143. $results = $query->select()
  144. ->contain('SiteArticles')
  145. ->hydrate(false)
  146. ->toArray();
  147. $expected = [
  148. [
  149. 'id' => 1,
  150. 'name' => 'mark',
  151. 'site_id' => 1,
  152. 'articles' => [
  153. [
  154. 'id' => 1,
  155. 'title' => 'First Article',
  156. 'body' => 'First Article Body',
  157. 'author_id' => 1,
  158. 'site_id' => 1
  159. ]
  160. ]
  161. ],
  162. [
  163. 'id' => 2,
  164. 'name' => 'juan',
  165. 'site_id' => 2,
  166. 'articles' => [],
  167. ],
  168. [
  169. 'id' => 3,
  170. 'name' => 'jose',
  171. 'site_id' => 2,
  172. 'articles' => [
  173. [
  174. 'id' => 2,
  175. 'title' => 'Second Article',
  176. 'body' => 'Second Article Body',
  177. 'author_id' => 3,
  178. 'site_id' => 2
  179. ]
  180. ]
  181. ],
  182. [
  183. 'id' => 4,
  184. 'name' => 'andy',
  185. 'site_id' => 1,
  186. 'articles' => [],
  187. ]
  188. ];
  189. $this->assertEquals($expected, $results);
  190. $results = $query->repository($table)
  191. ->select()
  192. ->contain(['SiteArticles' => ['conditions' => ['SiteArticles.id' => 2]]])
  193. ->hydrate(false)
  194. ->toArray();
  195. $expected[0]['articles'] = [];
  196. $this->assertEquals($expected, $results);
  197. $this->assertEquals($table->association('SiteArticles')->strategy(), $strategy);
  198. }
  199. /**
  200. * Tests that BelongsToMany associations are correctly eager loaded when multiple
  201. * foreignKeys are used
  202. *
  203. * @dataProvider strategiesProviderBelongsToMany
  204. * @return void
  205. */
  206. public function testBelongsToManyEager($strategy)
  207. {
  208. $articles = TableRegistry::get('SiteArticles');
  209. $tags = TableRegistry::get('SiteTags');
  210. $junction = TableRegistry::get('SiteArticlesTags');
  211. $articles->belongsToMany('SiteTags', [
  212. 'strategy' => $strategy,
  213. 'targetTable' => $tags,
  214. 'propertyName' => 'tags',
  215. 'through' => 'SiteArticlesTags',
  216. 'sort' => ['SiteTags.id' => 'asc'],
  217. 'foreignKey' => ['article_id', 'site_id'],
  218. 'targetForeignKey' => ['tag_id', 'site_id']
  219. ]);
  220. $query = new Query($this->connection, $articles);
  221. $results = $query->select()->contain('SiteTags')->hydrate(false)->toArray();
  222. $expected = [
  223. [
  224. 'id' => 1,
  225. 'author_id' => 1,
  226. 'title' => 'First Article',
  227. 'body' => 'First Article Body',
  228. 'site_id' => 1,
  229. 'tags' => [
  230. [
  231. 'id' => 1,
  232. 'name' => 'tag1',
  233. '_joinData' => ['article_id' => 1, 'tag_id' => 1, 'site_id' => 1],
  234. 'site_id' => 1
  235. ],
  236. [
  237. 'id' => 3,
  238. 'name' => 'tag3',
  239. '_joinData' => ['article_id' => 1, 'tag_id' => 3, 'site_id' => 1],
  240. 'site_id' => 1
  241. ]
  242. ]
  243. ],
  244. [
  245. 'id' => 2,
  246. 'title' => 'Second Article',
  247. 'body' => 'Second Article Body',
  248. 'author_id' => 3,
  249. 'site_id' => 2,
  250. 'tags' => [
  251. [
  252. 'id' => 4,
  253. 'name' => 'tag4',
  254. '_joinData' => ['article_id' => 2, 'tag_id' => 4, 'site_id' => 2],
  255. 'site_id' => 2
  256. ]
  257. ]
  258. ],
  259. [
  260. 'id' => 3,
  261. 'title' => 'Third Article',
  262. 'body' => 'Third Article Body',
  263. 'author_id' => 1,
  264. 'site_id' => 2,
  265. 'tags' => [],
  266. ],
  267. [
  268. 'id' => 4,
  269. 'title' => 'Fourth Article',
  270. 'body' => 'Fourth Article Body',
  271. 'author_id' => 3,
  272. 'site_id' => 1,
  273. 'tags' => [
  274. [
  275. 'id' => 1,
  276. 'name' => 'tag1',
  277. '_joinData' => ['article_id' => 4, 'tag_id' => 1, 'site_id' => 1],
  278. 'site_id' => 1
  279. ]
  280. ]
  281. ],
  282. ];
  283. $this->assertEquals($expected, $results);
  284. $this->assertEquals($articles->association('SiteTags')->strategy(), $strategy);
  285. }
  286. /**
  287. * Tests loding belongsTo with composite keys
  288. *
  289. * @dataProvider strategiesProviderBelongsTo
  290. * @return void
  291. */
  292. public function testBelongsToEager($strategy)
  293. {
  294. $table = TableRegistry::get('SiteArticles');
  295. $table->belongsTo('SiteAuthors', [
  296. 'propertyName' => 'author',
  297. 'strategy' => $strategy,
  298. 'foreignKey' => ['author_id', 'site_id']
  299. ]);
  300. $query = new Query($this->connection, $table);
  301. $results = $query->select()
  302. ->where(['SiteArticles.id IN' => [1, 2]])
  303. ->contain('SiteAuthors')
  304. ->hydrate(false)
  305. ->toArray();
  306. $expected = [
  307. [
  308. 'id' => 1,
  309. 'author_id' => 1,
  310. 'site_id' => 1,
  311. 'title' => 'First Article',
  312. 'body' => 'First Article Body',
  313. 'author' => [
  314. 'id' => 1,
  315. 'name' => 'mark',
  316. 'site_id' => 1
  317. ]
  318. ],
  319. [
  320. 'id' => 2,
  321. 'author_id' => 3,
  322. 'site_id' => 2,
  323. 'title' => 'Second Article',
  324. 'body' => 'Second Article Body',
  325. 'author' => [
  326. 'id' => 3,
  327. 'name' => 'jose',
  328. 'site_id' => 2
  329. ]
  330. ]
  331. ];
  332. $this->assertEquals($expected, $results);
  333. }
  334. /**
  335. * Tests loding hasOne with composite keys
  336. *
  337. * @dataProvider strategiesProviderHasOne
  338. * @return void
  339. */
  340. public function testHasOneEager($strategy)
  341. {
  342. $table = TableRegistry::get('SiteAuthors');
  343. $table->hasOne('SiteArticles', [
  344. 'propertyName' => 'first_article',
  345. 'strategy' => $strategy,
  346. 'foreignKey' => ['author_id', 'site_id']
  347. ]);
  348. $query = new Query($this->connection, $table);
  349. $results = $query->select()
  350. ->where(['SiteAuthors.id IN' => [1, 3]])
  351. ->contain('SiteArticles')
  352. ->hydrate(false)
  353. ->toArray();
  354. $expected = [
  355. [
  356. 'id' => 1,
  357. 'name' => 'mark',
  358. 'site_id' => 1,
  359. 'first_article' => [
  360. 'id' => 1,
  361. 'author_id' => 1,
  362. 'site_id' => 1,
  363. 'title' => 'First Article',
  364. 'body' => 'First Article Body'
  365. ]
  366. ],
  367. [
  368. 'id' => 3,
  369. 'name' => 'jose',
  370. 'site_id' => 2,
  371. 'first_article' => [
  372. 'id' => 2,
  373. 'author_id' => 3,
  374. 'site_id' => 2,
  375. 'title' => 'Second Article',
  376. 'body' => 'Second Article Body'
  377. ]
  378. ]
  379. ];
  380. $this->assertEquals($expected, $results);
  381. }
  382. /**
  383. * Tests that it is possible to insert a new row using the save method
  384. * if the entity has composite primary key
  385. *
  386. * @group save
  387. * @return void
  388. */
  389. public function testSaveNewEntity()
  390. {
  391. $entity = new \Cake\ORM\Entity([
  392. 'id' => 5,
  393. 'site_id' => 1,
  394. 'title' => 'Fifth Article',
  395. 'body' => 'Fifth Article Body',
  396. 'author_id' => 3,
  397. ]);
  398. $table = TableRegistry::get('SiteArticles');
  399. $this->assertSame($entity, $table->save($entity));
  400. $this->assertEquals($entity->id, 5);
  401. $row = $table->find('all')->where(['id' => 5, 'site_id' => 1])->first();
  402. $this->assertEquals($entity->toArray(), $row->toArray());
  403. }
  404. /**
  405. * Tests that it is possible to insert a new row using the save method
  406. * if the entity has composite primary key
  407. *
  408. * @group save
  409. * @expectedException \RuntimeException
  410. * @expectedExceptionMessage Cannot insert row, some of the primary key values are missing. Got (5, ), expecting (id, site_id)
  411. * @return void
  412. */
  413. public function testSaveNewEntityMissingKey()
  414. {
  415. $entity = new \Cake\ORM\Entity([
  416. 'id' => 5,
  417. 'title' => 'Fifth Article',
  418. 'body' => 'Fifth Article Body',
  419. 'author_id' => 3,
  420. ]);
  421. $table = TableRegistry::get('SiteArticles');
  422. $table->save($entity);
  423. }
  424. /**
  425. * Test simple delete with composite primary key
  426. *
  427. * @return void
  428. */
  429. public function testDelete()
  430. {
  431. $table = TableRegistry::get('SiteAuthors');
  432. $table->save(new \Cake\ORM\Entity(['id' => 1, 'site_id' => 2]));
  433. $entity = $table->get([1, 1]);
  434. $result = $table->delete($entity);
  435. $this->assertTrue($result);
  436. $this->assertEquals(4, $table->find('all')->count());
  437. $this->assertEmpty($table->find()->where(['id' => 1, 'site_id' => 1])->first());
  438. }
  439. /**
  440. * Test delete with dependent records having composite keys
  441. *
  442. * @return void
  443. */
  444. public function testDeleteDependent()
  445. {
  446. $table = TableRegistry::get('SiteAuthors');
  447. $table->hasMany('SiteArticles', [
  448. 'foreignKey' => ['author_id', 'site_id'],
  449. 'dependent' => true,
  450. ]);
  451. $entity = $table->get([3, 2]);
  452. $result = $table->delete($entity);
  453. $query = $table->association('SiteArticles')->find('all', [
  454. 'conditions' => [
  455. 'author_id' => $entity->id,
  456. 'site_id' => $entity->site_id
  457. ]
  458. ]);
  459. $this->assertNull($query->all()->first(), 'Should not find any rows.');
  460. }
  461. /**
  462. * Test generating a list of entities from a list of composite ids
  463. *
  464. * @return void
  465. */
  466. public function testOneGenerateBelongsToManyEntitiesFromIds()
  467. {
  468. $articles = TableRegistry::get('SiteArticles');
  469. $articles->entityClass(__NAMESPACE__ . '\OpenArticleEntity');
  470. $tags = TableRegistry::get('SiteTags');
  471. $junction = TableRegistry::get('SiteArticlesTags');
  472. $articles->belongsToMany('SiteTags', [
  473. 'targetTable' => $tags,
  474. 'propertyName' => 'tags',
  475. 'through' => 'SiteArticlesTags',
  476. 'foreignKey' => ['article_id', 'site_id'],
  477. 'targetForeignKey' => ['tag_id', 'site_id']
  478. ]);
  479. $data = [
  480. 'title' => 'Haz tags',
  481. 'body' => 'Some content here',
  482. 'tags' => ['_ids' => [[1, 1], [2, 2], [3, 1]]]
  483. ];
  484. $marshall = new Marshaller($articles);
  485. $result = $marshall->one($data, ['associated' => ['SiteTags']]);
  486. $this->assertCount(3, $result->tags);
  487. $this->assertInstanceOf('Cake\ORM\Entity', $result->tags[0]);
  488. $this->assertInstanceOf('Cake\ORM\Entity', $result->tags[1]);
  489. $this->assertInstanceOf('Cake\ORM\Entity', $result->tags[2]);
  490. $data = [
  491. 'title' => 'Haz tags',
  492. 'body' => 'Some content here',
  493. 'tags' => ['_ids' => [1, 2, 3]]
  494. ];
  495. $marshall = new Marshaller($articles);
  496. $result = $marshall->one($data, ['associated' => ['SiteTags']]);
  497. $this->assertEmpty($result->tags);
  498. }
  499. /**
  500. * Tests find('list') with composite keys
  501. *
  502. * @return void
  503. */
  504. public function testFindListCompositeKeys()
  505. {
  506. $table = new Table([
  507. 'table' => 'site_authors',
  508. 'connection' => $this->connection,
  509. ]);
  510. $table->displayField('name');
  511. $query = $table->find('list')
  512. ->hydrate(false)
  513. ->order('id');
  514. $expected = [
  515. '1;1' => 'mark',
  516. '2;2' => 'juan',
  517. '3;2' => 'jose',
  518. '4;1' => 'andy'
  519. ];
  520. $this->assertEquals($expected, $query->toArray());
  521. $table->displayField(['name', 'site_id']);
  522. $query = $table->find('list')
  523. ->hydrate(false)
  524. ->order('id');
  525. $expected = [
  526. '1;1' => 'mark;1',
  527. '2;2' => 'juan;2',
  528. '3;2' => 'jose;2',
  529. '4;1' => 'andy;1'
  530. ];
  531. $this->assertEquals($expected, $query->toArray());
  532. $query = $table->find('list', ['groupField' => ['site_id', 'site_id']])
  533. ->hydrate(false)
  534. ->order('id');
  535. $expected = [
  536. '1;1' => [
  537. '1;1' => 'mark;1',
  538. '4;1' => 'andy;1'
  539. ],
  540. '2;2' => [
  541. '2;2' => 'juan;2',
  542. '3;2' => 'jose;2'
  543. ]
  544. ];
  545. $this->assertEquals($expected, $query->toArray());
  546. }
  547. /**
  548. * Tests find('threaded') with composite keys
  549. *
  550. * @return void
  551. */
  552. public function testFindThreadedCompositeKeys()
  553. {
  554. $table = TableRegistry::get('SiteAuthors');
  555. $query = $this->getMock(
  556. '\Cake\ORM\Query',
  557. ['_addDefaultFields', 'execute'],
  558. [null, $table]
  559. );
  560. $items = new \Cake\Datasource\ResultSetDecorator([
  561. ['id' => 1, 'name' => 'a', 'site_id' => 1, 'parent_id' => null],
  562. ['id' => 2, 'name' => 'a', 'site_id' => 2, 'parent_id' => null],
  563. ['id' => 3, 'name' => 'a', 'site_id' => 1, 'parent_id' => 1],
  564. ['id' => 4, 'name' => 'a', 'site_id' => 2, 'parent_id' => 2],
  565. ['id' => 5, 'name' => 'a', 'site_id' => 2, 'parent_id' => 4],
  566. ['id' => 6, 'name' => 'a', 'site_id' => 1, 'parent_id' => 2],
  567. ['id' => 7, 'name' => 'a', 'site_id' => 1, 'parent_id' => 3],
  568. ['id' => 8, 'name' => 'a', 'site_id' => 2, 'parent_id' => 4],
  569. ]);
  570. $query->find('threaded', ['parentField' => ['parent_id', 'site_id']]);
  571. $formatter = $query->formatResults()[0];
  572. $expected = [
  573. [
  574. 'id' => 1,
  575. 'name' => 'a',
  576. 'site_id' => 1,
  577. 'parent_id' => null,
  578. 'children' => [
  579. [
  580. 'id' => 3,
  581. 'name' => 'a',
  582. 'site_id' => 1,
  583. 'parent_id' => 1,
  584. 'children' => [
  585. [
  586. 'id' => 7,
  587. 'name' => 'a',
  588. 'site_id' => 1,
  589. 'parent_id' => 3,
  590. 'children' => []
  591. ]
  592. ]
  593. ]
  594. ]
  595. ],
  596. [
  597. 'id' => 2,
  598. 'name' => 'a',
  599. 'site_id' => 2,
  600. 'parent_id' => null,
  601. 'children' => [
  602. [
  603. 'id' => 4,
  604. 'name' => 'a',
  605. 'site_id' => 2,
  606. 'parent_id' => 2,
  607. 'children' => [
  608. [
  609. 'id' => 5,
  610. 'name' => 'a',
  611. 'site_id' => 2,
  612. 'parent_id' => 4,
  613. 'children' => []
  614. ],
  615. [
  616. 'id' => 8,
  617. 'name' => 'a',
  618. 'site_id' => 2,
  619. 'parent_id' => 4,
  620. 'children' => []
  621. ]
  622. ]
  623. ]
  624. ]
  625. ],
  626. [
  627. 'id' => 6,
  628. 'name' => 'a',
  629. 'site_id' => 1,
  630. 'parent_id' => 2,
  631. 'children' => []
  632. ]
  633. ];
  634. $this->assertEquals($expected, $formatter($items)->toArray());
  635. }
  636. /**
  637. * Helper method to skip tests when connection is SQLite.
  638. *
  639. * @return void
  640. */
  641. public function skipIfSqlite()
  642. {
  643. $this->skipIf(
  644. $this->connection->driver() instanceof \Cake\Database\Driver\Sqlite,
  645. 'SQLite does not support the requrirements of this test.'
  646. );
  647. }
  648. }