Doctrine2 Embeddablesの紹介

Embeddableはエンティティに別のエンティティ(ValueObject)の情報を取り込み、1つのテーブルとしてマッピングする機能です。

Separating Concerns using Embeddables
You’ll mostly want to use them to reduce duplication or separating concerns. Value objects such as date range or address are the primary use case for this feature.

  • 同一のプロパティ定義を何度も書く必要がないため、コード量が減らせる
  • 同一のプロパティ定義をValueObject化する事により、関心が分離できる

ValueObjectはデザインパターンの1つです。 代表としてDomain-Driven Design(Eric Evans著)などの書籍で解説されています。

この記事を書いた環境

  • Symfony 2.6
  • Doctrine/ORM 2.5

このようなパターンに向いている

エンティティの一部をValueObjectとして抽出できる

  • 複数のエンティティが共通する値を持っている
  • エンティティの中にグループ化できるプロパティが集まっている

Embeddableの適用に最も向いていると思われるパターンの例です。 Customer BusinessPartner Staffという3つのエンティティがあり、それぞれに居住地・DM発送先などの住所に関する値を持っているとします。

Entity
  |--Customer (顧客)
  |    |--$dmZipCode, $dmPrefecture, $dmCity
  |
  |--BusinessPartner (提携先)
  |    |--$phoneNumber, $zipCode, $prefecture, $city
  |
  |--Staff (社員)
  |    |--$name, $zipCode, $prefecture, $city

住所をエンティティとして抽出しEmbeddableを適用すると、テーブルの構造を変更せずに一部の情報を別エンティティに分離する事ができます。

Entity
  |--Address
  |    |--$zipCode, $prefecture, $city
  |
  |--Customer
  |    |--$dmSendTo(Address)
  |
  |--BusinessPartner
  |    |--$phoneNumber, $address(Address)
  |
  |--Staff
  |    |--$name, $address(Address)

One-To-One との違い

一部の情報を別エンティティに分離する場合、Association MappingのOne-To-Oneを使っても似たような事が実現できます。 両者の違いは、作成されるテーブル構造にあります。

さきほどの例のStaff(社員情報)Address(住所情報)を元に違いを見てみましょう。

One-To-One

// Staff
+--------------------+--------------+------+-----+
| Field              | Type         | Null | Key |
+--------------------+--------------+------+-----+
| id                 | int(11)      | NO   | PRI |
| name               | varchar(255) | NO   |     |
+--------------------+--------------+------+-----+
    
// Address
+--------------------+--------------+------+-----+
| Field              | Type         | Null | Key |
+--------------------+--------------+------+-----+
| id                 | int(11)      | NO   | PRI |
| staff_id           | int(11)      | NO   | MUL |
| zip_code           | varchar(255) | NO   |     |
| prefecture         | varchar(255) | NO   |     |
| city               | varchar(255) | NO   |     |
+--------------------+--------------+------+-----+

Embeddable

// Staff
+--------------------+--------------+------+-----+
| Field              | Type         | Null | Key |
+--------------------+--------------+------+-----+
| id                 | int(11)      | NO   | PRI |
| name               | varchar(255) | NO   |     |
| address_zip_code   | varchar(255) | NO   |     |
| address_prefecture | varchar(255) | NO   |     |
| address_city       | varchar(255) | NO   |     |
+--------------------+--------------+------+-----+

One-To-Oneは分割したエンティティがそれぞれテーブルとして作成されるのに対し、Embeddableは1つのテーブルとして作成されます。

エンティティの分離とEmbeddablesの適用

以降は既存のエンティティから一部を分離しEmbeddablesを適用する過程を紹介します。

分離対象のエンティティは、氏名や住所などを持ったStaff(社員情報)です。

<?php
namespace MyProject\Bundle\AcmeDemoBundle\Entity

class Staff
{
	private $id;
	private $name;
	private $zipCode;
	private $prefecture;
	private $city;

1. 住所に関する部分をValueObject化

まずStaffから住所に関する部分をAddressに移動します。

<?php
namespace MyProject\Bundle\AcmeDemoBundle\Entity

class Staff
{
	private $id;
	private $name;	
	// セッター・ゲッターの記載は省略
<?php
namespace MyProject\Bundle\AcmeDemoBundle\Entity

class Address
{
	private $zipCode;
	private $prefecture;
	private $city;
	// セッター・ゲッターの記載は省略

注意点 1 generateコマンドで作成した場合はマッピングのアノテーションと自動生成される$idを削除してください。

<?php
/**
- * @ORM\Table()
- * @ORM\Entity
 */
class Address
{
-    /**
-     * @var integer
-     * @ORM\Column(name="id", type="integer")
-     * @ORM\Id
-     * @ORM\GeneratedValue(strategy="AUTO")
-     */
-    private $id;

注意点 2 ValueObject化したクラスはEntityの下に配置する必要があります。

<?php
namespace MyProject\Bundle\AcmeDemoBundle\Entity

2. Embeddablesを適用

埋め込み先のプロパティにEmbeddedアノテーション、ValueObject側にEmbeddableアノテーションをそれぞれ指定します。

<?php
+ use MyProject\Bundle\AcmeDemoBundle\Entity\Address;
    
class Staff
{
+  /**
+   * @var Address
+   * @ORM\Embedded(class="Address")
+   */
+  private $address;
<?php
+ /**
+  * @ORM\Embeddable()
+  */
class Address
{

3. Staffが常に1つのAddressを持つ状態にする

<?php
class Staff
{
+  public function __construct()
+  {
+    $this->address = new Address();
+  }

4. スキーマの更新

doctrine:schema:update --forceを実行してみましょう。 以下のようなテーブルが作成されます。

// Staff
+--------------------+--------------+------+-----+
| Field              | Type         | Null | Key |
+--------------------+--------------+------+-----+
| id                 | int(11)      | NO   | PRI |
| name               | varchar(255) | NO   |     |
| address_zip_code   | varchar(255) | NO   |     |
| address_prefecture | varchar(255) | NO   |     |
| address_city       | varchar(255) | NO   |     |
+--------------------+--------------+------+-----+

columnPrefixオプション

フィールド名のプレフィクスを変更する事ができます。

指定なし (クラス名がプレフィクスになる)

<?php
class Staff
{
  /**
   * @ORM\Embedded(class="Address", columnPrefix="")
   */
  private $address;

+--------------------+--------------+------+-----+
| Field              | Type         | Null | Key |
+--------------------+--------------+------+-----+
| id                 | int(11)      | NO   | PRI |
| name               | varchar(255) | NO   |     |
| address_zip_code   | varchar(255) | NO   |     |
| address_prefecture | varchar(255) | NO   |     |
| address_city       | varchar(255) | NO   |     |
+--------------------+--------------+------+-----+

プレフィクスを付けない

<?php
class Staff
{
  /**
   * @ORM\Embedded(class="Address", columnPrefix=false)
   */
  private $address;
    
+------------+--------------+------+-----+
| Field      | Type         | Null | Key |
+------------+--------------+------+-----+
| id         | int(11)      | NO   | PRI |
| name       | varchar(255) | NO   |     |
| zip_code   | varchar(255) | NO   |     |
| prefecture | varchar(255) | NO   |     |
| city       | varchar(255) | NO   |     |
+------------+--------------+------+-----+

任意の文字列

<?php
class Staff
{
  /**
   * @ORM\Embedded(class="Address", columnPrefix="latest_address_")
   */
  private $address;
    
+---------------------------+--------------+------+-----+
| Field                     | Type         | Null | Key |
+---------------------------+--------------+------+-----+
| id                        | int(11)      | NO   | PRI |
| name                      | varchar(255) | NO   |     |
| latest_address_zip_code   | varchar(255) | NO   |     |
| latest_address_prefecture | varchar(255) | NO   |     |
| latest_address_city       | varchar(255) | NO   |     |
+---------------------------+--------------+------+-----+

終わりに

Embeddablesはたくさんの便利機能を持っている訳ではありません。 ValueObjectをエンティティの一部として1つのテーブルにマッピングしてくれる、ただそれだけの機能です。

当社の技術ブログでは以前にDoctrine2のInheritanceMappingといった方法も紹介しています。 Doctrineのいろいろな手法を知り、その場に合ったパターンをすぐに取り出せるようにしたいですね。