CompositeKeysTest.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  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. 'foreignKey' => ['article_id', 'site_id'],
  217. 'targetForeignKey' => ['tag_id', 'site_id']
  218. ]);
  219. $query = new Query($this->connection, $articles);
  220. $results = $query->select()->contain('SiteTags')->hydrate(false)->toArray();
  221. $expected = [
  222. [
  223. 'id' => 1,
  224. 'author_id' => 1,
  225. 'title' => 'First Article',
  226. 'body' => 'First Article Body',
  227. 'site_id' => 1,
  228. 'tags' => [
  229. [
  230. 'id' => 1,
  231. 'name' => 'tag1',
  232. '_joinData' => ['article_id' => 1, 'tag_id' => 1, 'site_id' => 1],
  233. 'site_id' => 1
  234. ],
  235. [
  236. 'id' => 3,
  237. 'name' => 'tag3',
  238. '_joinData' => ['article_id' => 1, 'tag_id' => 3, 'site_id' => 1],
  239. 'site_id' => 1
  240. ]
  241. ]
  242. ],
  243. [
  244. 'id' => 2,
  245. 'title' => 'Second Article',
  246. 'body' => 'Second Article Body',
  247. 'author_id' => 3,
  248. 'site_id' => 2,
  249. 'tags' => [
  250. [
  251. 'id' => 4,
  252. 'name' => 'tag4',
  253. '_joinData' => ['article_id' => 2, 'tag_id' => 4, 'site_id' => 2],
  254. 'site_id' => 2
  255. ]
  256. ]
  257. ],
  258. [
  259. 'id' => 3,
  260. 'title' => 'Third Article',
  261. 'body' => 'Third Article Body',
  262. 'author_id' => 1,
  263. 'site_id' => 2,
  264. 'tags' => [],
  265. ],
  266. [
  267. 'id' => 4,
  268. 'title' => 'Fourth Article',
  269. 'body' => 'Fourth Article Body',
  270. 'author_id' => 3,
  271. 'site_id' => 1,
  272. 'tags' => [
  273. [
  274. 'id' => 1,
  275. 'name' => 'tag1',
  276. '_joinData' => ['article_id' => 4, 'tag_id' => 1, 'site_id' => 1],
  277. 'site_id' => 1
  278. ]
  279. ]
  280. ],
  281. ];
  282. $this->assertEquals($expected, $results);
  283. $this->assertEquals($articles->association('SiteTags')->strategy(), $strategy);
  284. }
  285. /**
  286. * Tests loding belongsTo with composite keys
  287. *
  288. * @dataProvider strategiesProviderBelongsTo
  289. * @return void
  290. */
  291. public function testBelongsToEager($strategy)
  292. {
  293. $table = TableRegistry::get('SiteArticles');
  294. $table->belongsTo('SiteAuthors', [
  295. 'propertyName' => 'author',
  296. 'strategy' => $strategy,
  297. 'foreignKey' => ['author_id', 'site_id']
  298. ]);
  299. $query = new Query($this->connection, $table);
  300. $results = $query->select()
  301. ->where(['SiteArticles.id IN' => [1, 2]])
  302. ->contain('SiteAuthors')
  303. ->hydrate(false)
  304. ->toArray();
  305. $expected = [
  306. [
  307. 'id' => 1,
  308. 'author_id' => 1,
  309. 'site_id' => 1,
  310. 'title' => 'First Article',
  311. 'body' => 'First Article Body',
  312. 'author' => [
  313. 'id' => 1,
  314. 'name' => 'mark',
  315. 'site_id' => 1
  316. ]
  317. ],
  318. [
  319. 'id' => 2,
  320. 'author_id' => 3,
  321. 'site_id' => 2,
  322. 'title' => 'Second Article',
  323. 'body' => 'Second Article Body',
  324. 'author' => [
  325. 'id' => 3,
  326. 'name' => 'jose',
  327. 'site_id' => 2
  328. ]
  329. ]
  330. ];
  331. $this->assertEquals($expected, $results);
  332. }
  333. /**
  334. * Tests loding hasOne with composite keys
  335. *
  336. * @dataProvider strategiesProviderHasOne
  337. * @return void
  338. */
  339. public function testHasOneEager($strategy)
  340. {
  341. $table = TableRegistry::get('SiteAuthors');
  342. $table->hasOne('SiteArticles', [
  343. 'propertyName' => 'first_article',
  344. 'strategy' => $strategy,
  345. 'foreignKey' => ['author_id', 'site_id']
  346. ]);
  347. $query = new Query($this->connection, $table);
  348. $results = $query->select()
  349. ->where(['SiteAuthors.id IN' => [1, 3]])
  350. ->contain('SiteArticles')
  351. ->hydrate(false)
  352. ->toArray();
  353. $expected = [
  354. [
  355. 'id' => 1,
  356. 'name' => 'mark',
  357. 'site_id' => 1,
  358. 'first_article' => [
  359. 'id' => 1,
  360. 'author_id' => 1,
  361. 'site_id' => 1,
  362. 'title' => 'First Article',
  363. 'body' => 'First Article Body'
  364. ]
  365. ],
  366. [
  367. 'id' => 3,
  368. 'name' => 'jose',
  369. 'site_id' => 2,
  370. 'first_article' => [
  371. 'id' => 2,
  372. 'author_id' => 3,
  373. 'site_id' => 2,
  374. 'title' => 'Second Article',
  375. 'body' => 'Second Article Body'
  376. ]
  377. ]
  378. ];
  379. $this->assertEquals($expected, $results);
  380. }
  381. /**
  382. * Tests that it is possible to insert a new row using the save method
  383. * if the entity has composite primary key
  384. *
  385. * @group save
  386. * @return void
  387. */
  388. public function testSaveNewEntity()
  389. {
  390. $entity = new \Cake\ORM\Entity([
  391. 'id' => 5,
  392. 'site_id' => 1,
  393. 'title' => 'Fifth Article',
  394. 'body' => 'Fifth Article Body',
  395. 'author_id' => 3,
  396. ]);
  397. $table = TableRegistry::get('SiteArticles');
  398. $this->assertSame($entity, $table->save($entity));
  399. $this->assertEquals($entity->id, 5);
  400. $row = $table->find('all')->where(['id' => 5, 'site_id' => 1])->first();
  401. $this->assertEquals($entity->toArray(), $row->toArray());
  402. }
  403. /**
  404. * Tests that it is possible to insert a new row using the save method
  405. * if the entity has composite primary key
  406. *
  407. * @group save
  408. * @expectedException \RuntimeException
  409. * @expectedExceptionMessage Cannot insert row, some of the primary key values are missing. Got (5, ), expecting (id, site_id)
  410. * @return void
  411. */
  412. public function testSaveNewEntityMissingKey()
  413. {
  414. $entity = new \Cake\ORM\Entity([
  415. 'id' => 5,
  416. 'title' => 'Fifth Article',
  417. 'body' => 'Fifth Article Body',
  418. 'author_id' => 3,
  419. ]);
  420. $table = TableRegistry::get('SiteArticles');
  421. $table->save($entity);
  422. }
  423. /**
  424. * Test simple delete with composite primary key
  425. *
  426. * @return void
  427. */
  428. public function testDelete()
  429. {
  430. $table = TableRegistry::get('SiteAuthors');
  431. $table->save(new \Cake\ORM\Entity(['id' => 1, 'site_id' => 2]));
  432. $entity = $table->get([1, 1]);
  433. $result = $table->delete($entity);
  434. $this->assertTrue($result);
  435. $this->assertEquals(4, $table->find('all')->count());
  436. $this->assertEmpty($table->find()->where(['id' => 1, 'site_id' => 1])->first());
  437. }
  438. /**
  439. * Test delete with dependent records having composite keys
  440. *
  441. * @return void
  442. */
  443. public function testDeleteDependent()
  444. {
  445. $table = TableRegistry::get('SiteAuthors');
  446. $table->hasMany('SiteArticles', [
  447. 'foreignKey' => ['author_id', 'site_id'],
  448. 'dependent' => true,
  449. ]);
  450. $entity = $table->get([3, 2]);
  451. $result = $table->delete($entity);
  452. $query = $table->association('SiteArticles')->find('all', [
  453. 'conditions' => [
  454. 'author_id' => $entity->id,
  455. 'site_id' => $entity->site_id
  456. ]
  457. ]);
  458. $this->assertNull($query->all()->first(), 'Should not find any rows.');
  459. }
  460. /**
  461. * Test generating a list of entities from a list of composite ids
  462. *
  463. * @return void
  464. */
  465. public function testOneGenerateBelongsToManyEntitiesFromIds()
  466. {
  467. $articles = TableRegistry::get('SiteArticles');
  468. $articles->entityClass(__NAMESPACE__ . '\OpenArticleEntity');
  469. $tags = TableRegistry::get('SiteTags');
  470. $junction = TableRegistry::get('SiteArticlesTags');
  471. $articles->belongsToMany('SiteTags', [
  472. 'targetTable' => $tags,
  473. 'propertyName' => 'tags',
  474. 'through' => 'SiteArticlesTags',
  475. 'foreignKey' => ['article_id', 'site_id'],
  476. 'targetForeignKey' => ['tag_id', 'site_id']
  477. ]);
  478. $data = [
  479. 'title' => 'Haz tags',
  480. 'body' => 'Some content here',
  481. 'tags' => ['_ids' => [[1, 1], [2, 2], [3, 1]]]
  482. ];
  483. $marshall = new Marshaller($articles);
  484. $result = $marshall->one($data, ['associated' => ['SiteTags']]);
  485. $this->assertCount(3, $result->tags);
  486. $this->assertInstanceOf('Cake\ORM\Entity', $result->tags[0]);
  487. $this->assertInstanceOf('Cake\ORM\Entity', $result->tags[1]);
  488. $this->assertInstanceOf('Cake\ORM\Entity', $result->tags[2]);
  489. $data = [
  490. 'title' => 'Haz tags',
  491. 'body' => 'Some content here',
  492. 'tags' => ['_ids' => [1, 2, 3]]
  493. ];
  494. $marshall = new Marshaller($articles);
  495. $result = $marshall->one($data, ['associated' => ['SiteTags']]);
  496. $this->assertEmpty($result->tags);
  497. }
  498. /**
  499. * Tests find('list') with composite keys
  500. *
  501. * @return void
  502. */
  503. public function testFindListCompositeKeys()
  504. {
  505. $table = new Table([
  506. 'table' => 'site_authors',
  507. 'connection' => $this->connection,
  508. ]);
  509. $table->displayField('name');
  510. $query = $table->find('list')
  511. ->hydrate(false)
  512. ->order('id');
  513. $expected = [
  514. '1;1' => 'mark',
  515. '2;2' => 'juan',
  516. '3;2' => 'jose',
  517. '4;1' => 'andy'
  518. ];
  519. $this->assertEquals($expected, $query->toArray());
  520. $table->displayField(['name', 'site_id']);
  521. $query = $table->find('list')
  522. ->hydrate(false)
  523. ->order('id');
  524. $expected = [
  525. '1;1' => 'mark;1',
  526. '2;2' => 'juan;2',
  527. '3;2' => 'jose;2',
  528. '4;1' => 'andy;1'
  529. ];
  530. $this->assertEquals($expected, $query->toArray());
  531. $query = $table->find('list', ['groupField' => ['site_id', 'site_id']])
  532. ->hydrate(false)
  533. ->order('id');
  534. $expected = [
  535. '1;1' => [
  536. '1;1' => 'mark;1',
  537. '4;1' => 'andy;1'
  538. ],
  539. '2;2' => [
  540. '2;2' => 'juan;2',
  541. '3;2' => 'jose;2'
  542. ]
  543. ];
  544. $this->assertEquals($expected, $query->toArray());
  545. }
  546. /**
  547. * Tests find('threaded') with composite keys
  548. *
  549. * @return void
  550. */
  551. public function testFindThreadedCompositeKeys()
  552. {
  553. $table = TableRegistry::get('SiteAuthors');
  554. $query = $this->getMock(
  555. '\Cake\ORM\Query',
  556. ['_addDefaultFields', 'execute'],
  557. [null, $table]
  558. );
  559. $items = new \Cake\Datasource\ResultSetDecorator([
  560. ['id' => 1, 'name' => 'a', 'site_id' => 1, 'parent_id' => null],
  561. ['id' => 2, 'name' => 'a', 'site_id' => 2, 'parent_id' => null],
  562. ['id' => 3, 'name' => 'a', 'site_id' => 1, 'parent_id' => 1],
  563. ['id' => 4, 'name' => 'a', 'site_id' => 2, 'parent_id' => 2],
  564. ['id' => 5, 'name' => 'a', 'site_id' => 2, 'parent_id' => 4],
  565. ['id' => 6, 'name' => 'a', 'site_id' => 1, 'parent_id' => 2],
  566. ['id' => 7, 'name' => 'a', 'site_id' => 1, 'parent_id' => 3],
  567. ['id' => 8, 'name' => 'a', 'site_id' => 2, 'parent_id' => 4],
  568. ]);
  569. $query->find('threaded', ['parentField' => ['parent_id', 'site_id']]);
  570. $formatter = $query->formatResults()[0];
  571. $expected = [
  572. [
  573. 'id' => 1,
  574. 'name' => 'a',
  575. 'site_id' => 1,
  576. 'parent_id' => null,
  577. 'children' => [
  578. [
  579. 'id' => 3,
  580. 'name' => 'a',
  581. 'site_id' => 1,
  582. 'parent_id' => 1,
  583. 'children' => [
  584. [
  585. 'id' => 7,
  586. 'name' => 'a',
  587. 'site_id' => 1,
  588. 'parent_id' => 3,
  589. 'children' => []
  590. ]
  591. ]
  592. ]
  593. ]
  594. ],
  595. [
  596. 'id' => 2,
  597. 'name' => 'a',
  598. 'site_id' => 2,
  599. 'parent_id' => null,
  600. 'children' => [
  601. [
  602. 'id' => 4,
  603. 'name' => 'a',
  604. 'site_id' => 2,
  605. 'parent_id' => 2,
  606. 'children' => [
  607. [
  608. 'id' => 5,
  609. 'name' => 'a',
  610. 'site_id' => 2,
  611. 'parent_id' => 4,
  612. 'children' => []
  613. ],
  614. [
  615. 'id' => 8,
  616. 'name' => 'a',
  617. 'site_id' => 2,
  618. 'parent_id' => 4,
  619. 'children' => []
  620. ]
  621. ]
  622. ]
  623. ]
  624. ],
  625. [
  626. 'id' => 6,
  627. 'name' => 'a',
  628. 'site_id' => 1,
  629. 'parent_id' => 2,
  630. 'children' => []
  631. ]
  632. ];
  633. $this->assertEquals($expected, $formatter($items)->toArray());
  634. }
  635. /**
  636. * Helper method to skip tests when connection is SQLite.
  637. *
  638. * @return void
  639. */
  640. public function skipIfSqlite()
  641. {
  642. $this->skipIf(
  643. $this->connection->driver() instanceof \Cake\Database\Driver\Sqlite,
  644. 'SQLite does not support the requrirements of this test.'
  645. );
  646. }
  647. }