自己紹介
TECHNICAへの投稿は初めてとなります。「ガリレオ」です。よろしくお願いします。
普段の業務ではスマートフォンアプリ開発運用チームでエンジニアとして働いております。
今回は以前メインエンジニアとして関わったHEROES解放戦線(以下HEROES)という
ソーシャルゲーム開発プロジェクトで、Doctrine2を導入した事例について書かせていただきます。
Doctrine2導入の背景
もともとCakePhp+smartyで開発を行っていましたが、
という目的でDoctrine2の導入を行いました。
選定理由は大きく以下の3点ですが、特に運用開発中のプロダクトであるため、開発効率を落とさないことを重視しています。
Doctrine2とは
公式サイト:http://www.doctrine-project.org/
Symfony2日本語ドキュメント内の記事:http://docs.symfony.gr.jp/symfony2/book/doctrine.html
Doctrine2はDBマイグレーションを備えた高機能なORMです。
Symfony2で利用されていることで有名ですが、
完全に独立したプロジェクトのため、
今回のようにDoctrineのみを導入することも可能です。
機能によっては大幅にパフォーマンスが悪化します。
このため、プロジェクトルールとして個人の開発環境であってもAPCを利用させるようにしています。
CakePHPにDoctine2を入れてみた
インストール
Composerを利用して導入しましたのでcomposer.jsonを作成します
{ "config": { "vendor-dir": "Vendor" }, "require": { "doctrine/orm": "2.3.3", "doctrine/dbal": "2.3.3", "symfony/yaml": "2.2.0", "gedmo/doctrine-extensions": "2.3.5" } }
HEROES用のカスタマイズ
HEROESプロジェクト用にCakeの各ベースクラスに手を入れます。
AppControllerの改修
Controller上で簡単にEntityManagerを取得するメソッドを準備します。
AppController.php
class AppController extends Controller { public $entityManager = null; /** * Doctrineのエンティティマネージャを取得する * * @return Doctrine\ORM\EntityManager */ public function getEntityManager() { if (!empty($this->entityManager)) { return $this-> entityManager; } require_once (__DIR__ . '/../Config/doctrine.php'); return Doctrine::getEntityManager(); } }
Componentの改修
Componentは呼び出し元のコントローラからEntityManagerを取得するようにしておきます。
Component.php
class Component extends Object { /** * @var AppController */ public $controller; public function initialize($controller) { $this->controller = $controller; } protected function getEntityManager() { return $this->controller ? $this->controller->getEntityManager() : null; } }
AppEntityの改修
プロジェクト方針として、全テーブルに「作成時刻、更新時刻、削除時刻、論理削除フラグ」等を持つこととします。 こちらは全てのEntityの親クラスAppEntityに実装しておきます。 また、@PrePersist,@PreUpdateを利用して、作成時刻、更新時刻は自動的に反映されるようにしておきます。
\Entity\AppEntity.php ※長いので一部抜粋
class Component extends Object { /** * @var AppController */ public $controller; public function initialize($controller) { $this ->controller = $controller; } protected function getEntityManager() { return $this->controller ? $this->controller->getEntityManager() : null; } } use DoctrineORMMapping as ORM; /** * Doctrineエンティティ基底クラス * * @ORM\MappedSuperclass * @ORM\HasLifecycleCallbacks * @Gedmo\SoftDeleteable(fieldName="deleteTime") **/ abstract class AppEntity { /** * @var datetime 作成日時 * * @ORM\Column(name="create_time", type="datetime", nullable=true, options={"comment"="作成日時"}) */ protected $createTime = null; /** * @var datetime 更新日時 * * @ORM\Column(name="update_time", type="datetime", nullable=false, options={"comment"="更新日時"}) */ protected $updateTime = null; /** * @ORM\PrePersist */ public function setDefaultCreateTime() { $this ->createTime = $this->updateTime = new DateTime(); } /** * @ORM\PreUpdate */ public function setDefaultUpdateTime() { $this->updateTime = new DateTime(); } /** * Set CreateTime * * @param mixed $createTime * @return TimestampBase */ public function setCreateTime($createTime) { $this->createTime = $createTime; return $this; } /** * Get CreateTime * * @return */ public function getCreateTime() { return $this->createTime; } /** * Set UpdateTime * * @param mixed $updateTime * @return TimestampBase */ public function setUpdateTime($updateTime) { $this->updateTime = $updateTime; return $this; } /** * Get UpdateTime * @return */ public function getUpdateTime() { return $this->updateTime; } }
実際にDoctrine2を使ってみる
DB周りの開発フロー
Doctrine2を利用したDB周りの開発フローは次のようになります。
以下では例としてユーザーID毎のポイントを管理するTUserPointテーブルを作成してみます。
1.エンティティクラスを作る
エンティティクラスを作成し、テーブルのカラムとアノテーションを定義します。
アノテーションには以下の様なものが用意されています。
- @Table:テーブル定義。テーブル名、インデックスの定義など
- @index:インデックスの定義
- @Entity:エンティティの定義
- @Column:カラムの定義。カラム名、型桁、null許容など
他にも様々なアノテーションがあります。ここには書ききれないのでこちらのリンクをご参考ください。Annotations Reference
それでは実際にユーザーごとの得点を管理するTUserPointエンティティを作成しましょう。
TUserPoint.php
namespace Entity; use Doctrine\ORM\Mapping as ORM; /** * ユーザーポイントテーブル * * @ORM\Table(name="t_user_point", indexes={ * @ORM\index(name="IDX1", columns={"user_id"}), * @ORM\index(name="IDX2", columns={"point"}) * }) * @ORM\Entity(repositoryClass="Repository\TUserPointRepository") */ class TUserPoint extends \Entity\AppEntity { /** * @var integer ID * * @ORM\Column(name="id", type="integer", nullable=false) * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ protected $id; /** * @var integer ユーザーID * * @ORM\Column(name="user_id", type="integer", nullable=false) */ protected $userId; /** * @var integer ポイント * * @ORM\Column(name="point", type="integer", nullable=false) */ protected $point; }
Setter,Getterの自動生成 変数名はprotectedまたはprivateとしSetter,Getterは以下のコマンドで自動生成します。
Vendor/bin/doctrine orm:generate-entities --filter=[対象クラス名※前方一致] --extend=Entity\\AppEntity /path/to/project
2. Repositoryクラスの自動生成
リポジトリクラスは、DBアクセスのためのカスタムロジックを格納するクラスです。
HEROESでは、DQLは全てRepositoryに実装することとしています。 Repositoryクラスは下記コマンドで自動的に生成されます。
Vendor/bin/doctrine orm:generate-repositories ./
※リポジトリを生成するためには事前に@Entityのアノテーションへ以下の記述が必要です。
@Entity(repositoryClass="Repository\[エンティティクラス名]Repository")
3. Proxyクラスの自動生成
特にプログラマが手を入れる必要はありません。※エンティティを更新するたびに再生成する必要があります。
Vendor/bin/doctrine orm:generate-proxies
Vendor/bin/doctrine migrations:diff
Version[日付].phpというソースが生成されます。必要に応じてDDLの修正後マイグレーションを実行します。
5. マイグレーションの実行
また、この時点でバージョン管理テーブルに発行済のマイグレーションソースのIDが追加されます。
※バージョン管理テーブルのテーブル名などはmigrations.xml または migrations.ymlで設定できます。
Vendor/bin/doctrine migrations:migrate
これでTUserPoint.phpに対応したテーブルt_user_pointが作成されます。
DESC t_user_point; Field Type Null Key Default Extra id int(11) NO PRI NULL auto_increment user_id int(11) NO MUL NULL point int(11) NO MUL NULL create_time datetime YES NULL create_user_id int(10) unsigned NO NULL update_time datetime NO NULL update_user_id int(10) unsigned NO NULL delete_time datetime YES NULL delete_user_id int(10) unsigned YES NULL delete_flg int(11) NO NULL
以降はentityManagerを介して永続化を行っていきます。
ハマリポイント
以上の準備でCakePHP上支障なくDoctrineを利用できるようになりました。
ただし一点、本番データを利用したテスト中にentityManager内でメモリリークが頻発したので
こちらの対処法について書かせていただきます。
結論として
コミット後に適宜メモリ解放を行わないとメモリ使用量が増加し続ける
ということでした。 以下のコードでentityManager がメモリ開放されます。
$this->getEntityManager()->clear();
実際のプログラムにではflush,commit後に都度開放するようにしました。
public function submitAction { while (1) { ・・・ $this->getEntityManager()->flush(); $this->getEntityManager()->commit(); $this->getEntityManager()->clear(); } $this->getEntityManager()->flush(); $this->getEntityManager()->commit(); $this->getEntityManager()->clear(); }
最後に
いかがでしたでしょうか。
今回は実際にプロジェクトで動いているコードから、導入について書かせていただきました。
Doctrine2の導入事例として参考にしていただければ幸いです。