10分ぐらいで学べるSymfony2 〜Doctrine2のassociationsを見てみる〜

masterとcategoryのように複数テーブルに情報が分かれている場合でもDoctrine2はよろしくMAPしてくれます。
ただ、ORMを使っているとSQLが直接見えない分、残念なSQLになってしまう事があるので挙動を調べたメモです。


Doctirne2のテーブル間の関係性において「1対1」「1対多」「多対多」の3つがあります。
今回は「1対多」で参照の場合のメモとなります。


1. テーブル構成

hoge1が1でhoge_typeが多の関係性で初期データやデータ定義は下記とします。

# テーブル定義
mysql> desc hoge1;
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int(11)     | NO   | PRI | NULL    | auto_increment |
| name  | varchar(32) | YES  |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

mysql> desc hoge1_type;
+----------+-------------+------+-----+---------+----------------+
| Field    | Type        | Null | Key | Default | Extra          |
+----------+-------------+------+-----+---------+----------------+
| id       | int(11)     | NO   | PRI | NULL    | auto_increment |
| hoge1_id | int(11)     | NO   | MUL | NULL    |                |
| name     | varchar(32) | YES  |     | NULL    |                |
+----------+-------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

# テーブルデー
mysql> select * from hoge1;
+----+---------+
| id | name    |
+----+---------+
|  1 | master1 |
|  2 | master2 |
|  3 | master3 |
|  4 | master4 |
|  5 | master5 |
+----+---------+

mysql> select * from hoge1_type;
+----+----------+-------------+
| id | hoge1_id | name        |
+----+----------+-------------+
|  1 |        1 | hoge1_type1 |
|  2 |        2 | hoge2_type1 |
|  3 |        2 | hoge2_type2 |
|  4 |        4 | hoge4_type1 |
|  5 |        4 | hoge4_type2 |
|  6 |        4 | hoge4_type3 |
|  7 |        4 | hoge4_type4 |
+----+----------+-------------+
7 rows in set (0.00 sec)
mysql>


2. リレーション設定

「mappedBy」と「inversedBy」には相手側のプロパティ名が入るので注意しましょう。

# one側
Root\TestBundle\Entity\Hoge1:
  type: entity
  oneToMany:
    Hoge1Types:
      targetEntity: Hoge1Type
      mappedBy:     hoge1
      cascade:      ['all']

# many側
Root\TestBundle\Entity\Hoge1Type:
  type:  entity
  table: hoge1_type
  manyToOne:
    hoge1:
      targetEntity: Hoge1
      inversedBy:   Hoge1Types
      joinColumn:
        name: hoge1_id
        referencedColumnName: id


3. hoge1(one)からhoge1_type(many)のデータにアクセス

(1)typeデータにアクセスしない場合
typeのデータにアクセスしなければhoge1データだけをselectします。

# 処理
  $em = $this->getDoctrine()->getEntityManager();
  $all = $em->getRepository('RootTestBundle:Hoge1')->findAll();
  foreach($all as $hoge1)
  {
      print $hoge1->getName().'<br />';
  }

# 実行結果
master1
master2
master3
master4
master5

# 発行SQL
  SELECT t0.id AS id1, t0.name AS name2 FROM hoge1 t0


(2)typeデータにアクセスするがjoinしない場合

typeデータにアクセスする場合毎回SQLが発生します。
リストなどでTypeのデータが必要な場合limitの数だけSELECT文が発生して悲惨な事になりますので注意が必要です。

# 処理
  $em = $this->getDoctrine()->getEntityManager();
  $all = $em->getRepository('RootTestBundle:Hoge1')->findAll();
  foreach($all as $hoge1)
  {
      print $hoge1->getName().'<br />';
      foreach($hoge1->getHoge1Types() as $type)
          print $type->getName().'<br />';
  }

# 実行結果
master1
hoge1_type1
master2
hoge2_type1
hoge2_type2
master3
master4
hoge4_type1
hoge4_type2
hoge4_type3
hoge4_type4
master5

# 発行SQL
SELECT t0.id AS id1, t0.name AS name2 FROM hoge1 t0
SELECT t0.id AS id1, t0.hoge1_id AS hoge1_id2, t0.name AS name3, t0.hoge1_id AS hoge1_id4 FROM hoge1_type t0 WHERE t0.hoge1_id = ? ([1]) 
SELECT t0.id AS id1, t0.hoge1_id AS hoge1_id2, t0.name AS name3, t0.hoge1_id AS hoge1_id4 FROM hoge1_type t0 WHERE t0.hoge1_id = ? ([2]) 
SELECT t0.id AS id1, t0.hoge1_id AS hoge1_id2, t0.name AS name3, t0.hoge1_id AS hoge1_id4 FROM hoge1_type t0 WHERE t0.hoge1_id = ? ([3]) 
SELECT t0.id AS id1, t0.hoge1_id AS hoge1_id2, t0.name AS name3, t0.hoge1_id AS hoge1_id4 FROM hoge1_type t0 WHERE t0.hoge1_id = ? ([4]) 
SELECT t0.id AS id1, t0.hoge1_id AS hoge1_id2, t0.name AS name3, t0.hoge1_id AS hoge1_id4 FROM hoge1_type t0 WHERE t0.hoge1_id = ? ([5]) 


(3)INNER JOINする場合

INNER JOINすると1回SQLでデータを取得できますが、typeデータに関連データが無い場合のhoge1データが欠落するので注意が必要です。

# 処理
  $em  = $this->getDoctrine()->getEntityManager();
  $all = $em->createQuery('SELECT h1, t FROM RootTestBundle:Hoge1 h1 JOIN h1.Hoge1Types t')
            ->getResult()
  ;
  foreach($all as $hoge1)
  {
      print $hoge1->getName().'<br />';
      foreach($hoge1->getHoge1Types() as $type)
          print $type->getName().'<br />';
  }

# 実行結果
master1
hoge1_type1
master2
hoge2_type1
hoge2_type2
master4
hoge4_type1
hoge4_type2
hoge4_type3
hoge4_type4

# 発行SQL
SELECT h0_.id AS id0, h0_.name AS name1, h1_.id AS id2, h1_.hoge1_id AS hoge1_id3, h1_.name AS name4, h1_.hoge1_id AS hoge1_id5 
 FROM hoge1 h0_ INNER JOIN hoge1_type h1_ ON h0_.id = h1_.hoge1_id


(4)LEFT JOINする場合

これでmaster3、master5もデータを取得できました。
ただし、この場合はlimit句で5などと区切った場合master4のtypeデータに欠落が生じる事になるので注意が必要です。

# 処理
  $em  = $this->getDoctrine()->getEntityManager();
  $all = $em->createQuery('SELECT h1, t FROM RootTestBundle:Hoge1 h1 LEFT JOIN h1.Hoge1Types t')
            ->getResult()
  ;
  foreach($all as $hoge1)
  {
      print $hoge1->getName().'<br />';
      foreach($hoge1->getHoge1Types() as $type)
          print $type->getName().'<br />';
  }
# 実行結果
master1
hoge1_type1
master2
hoge2_type1
hoge2_type2
master3
master4
hoge4_type1
hoge4_type2
hoge4_type3
hoge4_type4
master5

# 発行SQL
SELECT h0_.id AS id0, h0_.name AS name1, h1_.id AS id2, h1_.hoge1_id AS hoge1_id3, h1_.name AS name4, h1_.hoge1_id AS hoge1_id5
 FROM hoge1 h0_ LEFT JOIN hoge1_type h1_ ON h0_.id = h1_.hoge1_id


4. hoge1_type(many)からhoge1(one)のデータにアクセス


(1)hoge1を取得
一度取得したhoge1はキャッシュされる。

# 処理
  $em  = $this->getDoctrine()->getEntityManager();
  $all = $em->createQuery('SELECT t FROM RootTestBundle:Hoge1Type t')
            ->getResult()
  ;
  foreach($all as $type)
  {
      print $type->getHoge1()->getName().':'. $type->getName().'<br />';
  }

# 実行結果
master1:hoge1_type1
master2:hoge2_type1
master2:hoge2_type2
master4:hoge4_type1
master4:hoge4_type2
master4:hoge4_type3

# 発行SQL
SELECT h0_.id AS id0, h0_.hoge1_id AS hoge1_id1, h0_.name AS name2, h0_.hoge1_id AS hoge1_id3 FROM hoge1_type h0_
SELECT t0.id AS id1, t0.name AS name2 FROM hoge1 t0 WHERE t0.id = ? (["1"])
SELECT t0.id AS id1, t0.name AS name2 FROM hoge1 t0 WHERE t0.id = ? (["2"])
SELECT t0.id AS id1, t0.name AS name2 FROM hoge1 t0 WHERE t0.id = ? (["4"])


(2)JOINしてhoge1を取得


JOINしてデータが重複した場合でも、よろしくORMにMAPしてくれる

# 処理
  $em  = $this->getDoctrine()->getEntityManager();
  $all = $em->createQuery('SELECT t, h1 FROM RootTestBundle:Hoge1Type t JOIN t.hoge1 h1')
            ->getResult()
  ;
  foreach($all as $type)
  {
      print $type->getHoge1()->getName().':'. $type->getName().'<br />';
  }

# 実行結果
master1:hoge1_type1
master2:hoge2_type1
master2:hoge2_type2
master4:hoge4_type1
master4:hoge4_type2
master4:hoge4_type3
master4:hoge4_type4

# 発行SQL
SELECT h0_.id AS id0, h0_.hoge1_id AS hoge1_id1, h0_.name AS name2, h1_.id AS id3, h1_.name AS name4, h0_.hoge1_id AS hoge1_id5 
FROM hoge1_type h0_ INNER JOIN hoge1 h1_ ON h0_.hoge1_id = h1_.id


以上です。