読者です 読者をやめる 読者になる 読者になる

Entityの継承を使ってマップするフィールドを切り替える (Doctrine ORM)

PHP

最近仕事で使った.NETのEntity Framework (DbContext API) に影響されて、DoctrineというORMを触っている。なんでもSymfonyので採用?されているのだとか。Symfony使ったことないので知らなかった。というかPHPでORM使うこと自体初めて。

さて、本題。

DoctrineのEntityは基本、1クラス1テーブルに対応する。@Column属性*1を定義したプロパティ(インスタンスフィールド)がテーブルのカラムとマップされる。

下記のようなUsersテーブルの場合、

CREATE TABLE Users (
id INTEGER PRIMARY KEY,
name VARCHAR(10),
profile TEXT
);

Entityクラスは下記のようになる。

<?php
/**
 * @Entity
 */
class Users {
  /**
   * @Column("integer")
   */
  private $id;

  /**
   * @Column("string")
   */
  private $name;

  /**
   * @Column("string")
   */
  private $profile;
}

このUsers Entityに対してfindBy*2すると、当然id、name、profileすべてのフィールドがDBから取得される。

これはこれで全然良いのだが、Usersの一覧を取得したいような場合、一覧表に出力する必要のなさそうなprofileまでガッツリ取得されてしまうのが困る。特にprofileがデカい場合、パフォーマンスも心配だ。

profileが別テーブルに切り出されていれば、遅延読み込みで関連を定義*3して必要なときだけ取得されるようにすれば良いが、今回のケースは不幸にもこのようなテーブル構造になっており、それを変更できない事情があるのでこの方法は採れない。

別テーブルへのAssociationではなく、フィールド単位でLAZY指定が可能であれば良かったのだが、ドキュメントを流し読みした感じではそのような指定はできなさそうであった。

となると、profile列をマップするEntityとマップしないEntityを用意し、用途によって使い分けるという方法をとるしかなさそうだ。

@Column属性を付与しない限りマッピングは行われないのだから、profile列をマップするクラスとしないクラス2つを作れば良い。ただ、単に2つ作ると保守性が悪いので、委譲か継承を使って共通化したい。委譲にするとどうしても関連になってしまうようなので、ここは継承を使わざるを得ない。

オブジェクト指向的要素を考慮せず、単にフィールドを共通化する目的においては、@MappedSuperclass属性の適用で十分と思われる。

残念なことに、@MappedSuperclass属性を付与したクラスはEntityになれないという制限があるようなので、下記のように冗長なクラス構造を構築せざるを得ない。

<?php
/**
 * @MappedSuperclass
 */
class UserBase {
  /**
   * @Column("integer")
   */
  private $id;

  /**
   * @Column("string")
   */
  private $name;
}

/**
 * @Entity
 */
class User extends UserBase {}

/**
 * @Entity
 */
class UserWithProfile extends UserBase {
  /**
   * @Column("string")
   */
  private $profile;

}

このようにEntityを定義し、一覧表示をしたい場合はUsersクラスをEntityManagerに与え、詳細表示をしたい場合はUsersWithProfileクラスをEntityManagerに与えればよい。

*1:厳密にはただのコメントだが、便宜上そう呼ぶ

*2:厳密にはUsers EntityのEntity Managerを取得して、それに対してfindByする

*3:@OneToOne属性にfetch="LAZY"を設定