複合主キーを持つテーブルのCRUD
Symfony2/Doctrineのドキュメントは基本的に単一カラムを主キーとするテーブルが対象となっていて、複合主キーを持つテーブルに対する説明はかなり少なくて、あったとしてもサラっと流されてしまってる感じです。まあ、エラーメッセージでググれば情報は大概は出てくるんですが、情報があちこちに散らばってる状況です。
で、実際に複合主キーを持つテーブルに対するCRUDを作って、単一カラムの主キーの場合との違いをまとめてみました。
Bundleのソースコード一式はgithubに上げてあります。
対象とするテーブル
以下のようなカラムkey1、key2が主キーとなるテーブル(composite_keys)です。
CREATE TABLE composite_keys ( key1 VARCHAR(2) NOT NULL, key2 VARCHAR(2) NOT NULL, name VARCHAR(10) NOT NULL ); ALTER TABLE composite_keys ADD CONSTRAINT pk_composite_keys PRIMARY KEY (key1, key2);
Entityクラス
基本的に@ORM\Idアノテーションを複合主キーとなるカラムに対して記述するだけです。ただし、データベースのシーケンスを使った自動採番はできません。
<?php namespace Acme\CompositePrimaryKeysBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; /** * composite_keysクラス * * @version $Id$ * @ORM\Table(name="composite_keys") * @ORM\Entity(repositoryClass="Acme\CompositePrimaryKeysBundle\Entity\CompositeKeysRepository") * @ORM\HasLifecycleCallbacks */ class CompositeKeys { /** * @var string $key1 複合キー1 * * @ORM\Id * @ORM\Column(name="key1", type="string", length=2, nullable=false) * @Assert\NotBlank(message="複合キー1は必須項目です") * @Assert\MaxLength(limit=2, message="複合キー1は{{ limit }}文字までです") * @Assert\Regex(pattern="#^[0-9]{2}$#", message="複合キー1の書式が正しくありません") */ private $key1; /** * @var string $key2 複合キー2 * * @ORM\Id * @ORM\Column(name="key2", type="string", length=2, nullable=false) * @Assert\NotBlank(message="複合キー2は必須項目です") * @Assert\MaxLength(limit=2, message="複合キー2は{{ limit }}文字までです") * @Assert\Regex(pattern="#^[0-9]{2}$#", message="複合キー2の書式が正しくありません") */ private $key2; /** * @var string $name 名称 * * @ORM\Column(name="name", type="string", length=10, nullable=false) * @Assert\NotBlank(message="名称は必須項目です") * @Assert\MaxLength(limit=10, message="名称は{{ limit }}文字までです") */ private $name; :
ソースコードはSymfony2_sample/CompositeKeys.php at master · shimooka/Symfony2_sample · GitHubです。
EntityRepository#findメソッド
引数にカラム名と値をペアとする連想配列を渡せます。以下、Controllerクラスでの例です。
<?php namespace Acme\CompositePrimaryKeysBundle\Controller; : class CompositeKeysController extends Controller { : /** * Finds and displays a CompositeKeys entity. * * @Route("/{key1}/{key2}/show", name="compositeKeys_show") * @Template() */ public function showAction($key1, $key2) { : /** * EntityRepository#findメソッドに配列で検索条件を渡す */ $entity = $this->getRepository() ->find(array('key1' => $key1, 'key2' => $key2)); : } : private function getEntityManager() { return $this->getDoctrine()->getEntityManager(); } private function getRepository($name = 'AcmeCompositePrimaryKeysBundle:CompositeKeys') { return $this->getEntityManager()->getRepository($name); } :
ソースコードはSymfony2_sample/CompositeKeysController.php at master · shimooka/Symfony2_sample · GitHubです。
KnpPaginatorBundleの対応
KnpPaginatorBundle便利ですね:-)
複合主キーを持つテーブルに対してKnpPaginatorBundleを使う場合、GitHub - KnpLabs/KnpPaginatorBundle: SEO friendly Symfony paginator to sort and paginateのUsage examplesにあるような
<?php $em = $this->get('doctrine.orm.entity_manager'); $dql = "SELECT a FROM VendorBlogBundle:Article a"; $query = $em->createQuery($dql); $paginator = $this->get('knp_paginator'); $pagination = $paginator->paginate( $query, $this->get('request')->query->get('page', 1)/*page number*/, 10/*limit per page*/ );
というコードを実行すると
Single id is not allowed on composite primary key in entity
というエラーが発生します。
調べてみるとどうやら複合主キーを持つテーブルのサポートが充分でないようで、ちょっとトリッキーなことをする必要があるようです。
1. KnpPaginatorBundleのdistinctオプションを無効にする
distinctオプションを無効にするコードは以下のような感じです。サービスからキー"knp_paginator"を指定することで得られるKnp\Component\Pager\Paginatorオブジェクトのpaginateメソッドの第4引数にオプションを指定します。
<?php namespace Acme\CompositePrimaryKeysBundle\Controller; : class CompositeKeysController extends Controller { : /** * Lists all CompositeKeys entities. * * @Route("/list", name="compositeKeys_list") * @Template("AcmeCompositePrimaryKeysBundle:CompositeKeys:index.html.twig") */ public function listAction() { : $paginator = $this->get('knp_paginator'); $pagination = $paginator->paginate( $query, $request->query->get('p', 1), // $request->query = $_GET 5, // items per page array('distinct' => false) // 複合主キーの場合は必須 ); : } :
ソースコードはSymfony2_sample/CompositeKeysController.php at master · shimooka/Symfony2_sample · GitHubです。
2. Doctrine\ORM\Query#setHintメソッドに全件数をキー"knp_paginator.count"として与えてやる
Single id is not allowed · Issue #27 · KnpLabs/KnpPaginatorBundle · GitHubにもコードがありますが、同一条件で全件数を取得し、それをsetHintメソッドを使ってEntityを取得するクエリに渡してやります。この時のキーは"knp_paginator.count"とします。
以下、EntityRepositoryクラスの1メソッドにまとめてしまっていますが、やっていることは同じです。
<?php namespace Acme\CompositePrimaryKeysBundle\Entity; use Doctrine\ORM\EntityRepository; class CompositeKeysRepository extends EntityRepository { public function findBySearchForm(array $data, $get_entity = true) { $binds = array(); $select_clause = 'a'; $order_clause = "ORDER BY a.key1 ASC, a.key2 ASC "; if (!$get_entity) { $select_clause = 'COUNT(a)'; $order_clause = null; } $query = "SELECT {$select_clause} FROM AcmeCompositePrimaryKeysBundle:CompositeKeys a " : $result = $this->getEntityManager() ->createQuery($query) ->setParameters($binds); if ($get_entity) { $result->setHint('knp_paginator.count', $this->findBySearchForm($data, false)); } else { $result = $result->getSingleScalarResult(); } return $result; } }
ソースコードはSymfony2_sample/CompositeKeysRepository.php at master · shimooka/Symfony2_sample · GitHubです。
まとめ
調査にかなり時間を取られてしまいましたが、複合主キーの場合でも思ったよりも難しくないようです。
これで作業を進められそうです:-)
おまけ:無理やりdoctrine:generate:crudでCRUDを作る
おまけというか、これがやりたかったことなんですが、doctrine:generate:crudを使って簡単にマスターデータの管理画面を作れないか試行錯誤してました。
で、ちょっと強引&完全自動ではないですが、ざっと以下のような感じである程度対応できると思います。
- 単一カラムを主キーとするEntityクラスをとりあえず作る
- doctrine:generate:crudコマンドでCRUDをとりあえず作る
- Entityクラスの主キーを本来の複合主キーに変更する
- 生成されたControllerクラスを修正する
- Routeアノテーションの"/{key1}/"を"/{key1}/{key2}/"に変更する
- 各ActionメソッドとcreateDeleteFormメソッドの受け取る引数を$key1, $key2に変更する
- EntityRepository#findメソッドの引数を正しく修正する
- その他、引数に"key1"を指定している部分に"key2"を追加する
- twigファイルも同様