CakePHPにDoctrine2をいれてみた

自己紹介

TECHNICAへの投稿は初めてとなります。「ガリレオ」です。よろしくお願いします。

普段の業務ではスマートフォンアプリ開発運用チームでエンジニアとして働いております。

今回は以前メインエンジニアとして関わったHEROES解放戦線(以下HEROES)という

ソーシャルゲーム開発プロジェクトで、Doctrine2を導入した事例について書かせていただきます。

Doctrine2導入の背景

もともとCakePhp+smartyで開発を行っていましたが、

  • DBアクセス周りのソース品質の均質化
  • DBマイグレーションの手順の整理(環境毎に手作業でDDLを実行していた)

という目的でDoctrine2の導入を行いました。

選定理由は大きく以下の3点ですが、特に運用開発中のプロダクトであるため、開発効率を落とさないことを重視しています。

  • Symfonyで多く利用されているため稼働実績があり信頼性が高い
  • ソースの自動生成機能に優れる
  • DBマイグレーションの手順を整理しやすい

Doctrine2とは

公式サイト:http://www.doctrine-project.org/

Symfony2日本語ドキュメント内の記事:http://docs.symfony.gr.jp/symfony2/book/doctrine.html

Doctrine2はDBマイグレーションを備えた高機能なORMです。

Symfony2で利用されていることで有名ですが、

完全に独立したプロジェクトのため、

今回のようにDoctrineのみを導入することも可能です。

APCの利用が推奨されていますが、APCを利用しない場合

機能によっては大幅にパフォーマンスが悪化します。

このため、プロジェクトルールとして個人の開発環境であっても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

4. マイグレーションDDLの自動生成

Entityのアノテーションを元にDDLを自動生成します。

Vendor/bin/doctrine migrations:diff

Version[日付].phpというソースが生成されます。必要に応じてDDLの修正後マイグレーションを実行します。

5. マイグレーションの実行

生成されたDDLを発行しマイグレーションを行います。

また、この時点でバージョン管理テーブルに発行済のマイグレーションソースの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の導入事例として参考にしていただければ幸いです。