tom__bo’s Blog

情報系学生が筋トレしたり、筋トレしたり筋トレしたことを書くブログ。もはやダイアリー

Doctrine ORMのチュートリアル(2)

前回に引き続きDoctrineのチュートリアルを和訳しつつ進めていきます。

(参考: Getting Started with Doctrine — Doctrine 2 ORM 2 documentation

 

Adding Bug and User Entities
 
では続けてBug, Userエンティティを追加していきます。
<?php
// src/Bug.php
/**
 * @Entity(repositoryClass="BugRepository") @Table(name="bugs")
 */
class Bug
{
    /**
     * @Id @Column(type="integer") @GeneratedValue
     * @var int
     */
    protected $id;
    /**
     * @Column(type="string")
     * @var string
     */
    protected $description;
    /**
     * @Column(type="datetime")
     * @var DateTime
     */
    protected $created;
    /**
     * @Column(type="string")
     * @var string
     */
    protected $status;

    public function getId()
    {
        return $this->id;
    }

    public function getDescription()
    {
        return $this->description;
    }

    public function setDescription($description)
    {
        $this->description = $description;
    }

    public function setCreated(DateTime $created)
    {
        $this->created = $created;
    }

    public function getCreated()
    {
        return $this->created;
    }

    public function setStatus($status)
    {
        $this->status = $status;
    }

    public function getStatus()
    {
        return $this->status;
    }
}
 
<?php
// src/User.php
/**
 * @Entity @Table(name="users")
 */
class User
{
    /**
     * @Id @GeneratedValue @Column(type="integer")
     * @var int
     */
    protected $id;
    /**
     * @Column(type="string")
     * @var string
     */
    protected $name;

    public function getId()
    {
        return $this->id;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setName($name)
    {
        $this->name = $name;
    }
}
 
 
これらのプロパティは単純なstringとintegerの値ですが、次はエンティティ間の関連を定義することで、動的なリレーションを扱っていきます。オブジェクト間の参照はDB内では外部キーによって行われますが、直接外部キーを操作することはすべきではありません。それぞれの外部キーに対してはDoctrineがManyToOneかOneToOneアソシエーションがあります。一方でOneToManyアソシエーションも使うことができます。さらにManyToManyアソシエーションによって、中間テーブルを介して二つのテーブルのそれぞれの外部キーをjoinすることもできます。Doctrineでリレーションを扱うには、ドメインモデルを拡張すればいいことがわかります。
 
<?php
// src/Bug.php
use Doctrine\Common\Collections\ArrayCollection;

class Bug
{
    // ... (previous code)

    protected $products;

    public function __construct()
    {
        $this->products = new ArrayCollection();
    }
}
 
<?php
// src/User.php
use Doctrine\Common\Collections\ArrayCollection;
class User
{
    // ... (previous code)

    protected $reportedBugs;
    protected $assignedBugs;

    public function __construct()
    {
        $this->reportedBugs = new ArrayCollection();
        $this->assignedBugs = new ArrayCollection();
    }
}
 
 
DBでエンティティが再構築されたときはいつでも、配列の代わりにエンティティを追加してくれます。ArrayCollectionに比べてこの実行はDoctrineORMが永続化に対して気づく価値のある変更を理解するのを手助けしてくれます。
 
永続化に対してコレクションにしか作業しないため、ドメインモデル内で矛盾する操作をしないように気を付ける必要があります。所有する側も逆の側もこれを常に頭に入れておく必要があります。Doctrineで操作するにあたって以下のような憶測に従う必要があります。これらの憶測はDoctrine独自のものではなく、ORMを扱うベストプラクティスです。
 

・コレクションに対するsaveやupdateはそのコレクションを所有しているエンティティがsave, updateされたときに実行される。

・saveしているエンティティの反対のエンティティはコレクションを変更する操作が実行されたりはしない

・1対1のリレーションにおいて、相手のエンティティの外部キーを所有しているほうが常に所有側となる。

・many-to-manyのリレーションにおいては両方が所有側となる。しかし、bi-directionalな多対多の関連は片方のみに許される。

・多対多のリレーションにおいて、「多」側はデフォルトで所有側となる、これはそちらが外部キーを持っているからである。

・1対多ではデフォルトで逆になる。なぜなら「多」側で外部キーをsaveするからである。もし、多対多によってテーブルをjoinし、片方にのみユニークな値を許可している場合、1対多のリレーションは所有側でのみ可能である。

 

 

このUsersとBugsの場合、ユーザから認定されたものと報告されたバグに対して、参照しあう関係になっていて、bi-directionalになっています。なので、これらを以下のように変更します

<?php
// src/Bug.php
class Bug
{
    // ... (previous code)

    protected $engineer;
    protected $reporter;

    public function setEngineer($engineer)
    {
        $engineer->assignedToBug($this);
        $this->engineer = $engineer;
    }

    public function setReporter($reporter)
    {
        $reporter->addReportedBug($this);
        $this->reporter = $reporter;
    }

    public function getEngineer()
    {
        return $this->engineer;
    }

    public function getReporter()
    {
        return $this->reporter;
    }
}
 
<?php
// src/User.php
class User
{
    // ... (previous code)

    private $reportedBugs = null;
    private $assignedBugs = null;

    public function addReportedBug($bug)
    {
        $this->reportedBugs[] = $bug;
    }

    public function assignedToBug($bug)
    {
        $this->assignedBugs[] = $bug;
    }
}
 
今回追加したメソッドには過去形を用いているが、これはチュートリ作者が実際の任命は済んでいて、永続化を保証することだけを示唆するためにしたそうです。これは作者独自のやり方なので、これをする方法は読者に任せるとなっています。
 
User#addReportedBug()とUser#assignedToBug()からわかるように、ユーザ側でのみこれを実行していて、所有側であるBug側には追加していないのがわかります。これらのメソッドとDoctrineを永続化のために呼ぶことでDBのコレクションが更新されることはないそうです。
 
Bug#setEngineer()とBug#setReporter()のみが正しく関係の情報を保存できます。
 
このBug#reporterとBug#engineerプロパティは多対1で、これは1人のUserを指しています。正規化された関連モデルでは、外部キーはBugテーブルに保存されるので、このオブジェクトリレーションモデルでも関係上の所有者側に用意しています。
利用者は常に自分のドメインモデルのユースケースがどちらを所有者側としているかをマッピングでも確認しなければなりません。今回の例だと、新しいバグが保存された時や、バグにエンジニアがアサインされた時には、UserをアップデートすることなしにBugをアップデートしたいです。これがリレーション上Bugが所有側にある理由になっています。
 
BugはBugからProductを指しているDB上で、uni-directionalな多対多のリレーションでProductsを参照しています。
<?php
// src/Bug.php
class Bug
{
    // ... (previous code)

    protected $products = null;

    public function assignToProduct($product)
    {
        $this->products[] = $product;
    }

    public function getProducts()
    {
        return $this->products;
    }
}
 
これで、ドメインモデルにおいて、与えられた要求は満たせました。
そこでProductにやったように、BugとUserにもメタデータマッピングを追加していきます。
<?php
// src/Bug.php
/**
 * @Entity @Table(name="bugs")
 **/
class Bug
{
    /**
     * @Id @Column(type="integer") @GeneratedValue
     **/
    protected $id;
    /**
     * @Column(type="string")
     **/
    protected $description;
    /**
     * @Column(type="datetime")
     **/
    protected $created;
    /**
     * @Column(type="string")
     **/
    protected $status;

    /**
     * @ManyToOne(targetEntity="User", inversedBy="assignedBugs")
     **/
    protected $engineer;

    /**
     * @ManyToOne(targetEntity="User", inversedBy="reportedBugs")
     **/
    protected $reporter;

    /**
     * @ManyToMany(targetEntity="Product")
     **/
    protected $products;

    // ... (other code)
}
 
 
これでエンティティが定義できました。”created”のフィールドにはdatetimeタイプを使っていますが、これはDBの”YYYY-mm-dd HH:mm:ss”のフォーマットから、PHPのDateTimeのインスタンスに変換されます。
 

フィールド定義の後にはユーザエンティティへの2つの参照が定義されています。これらはmany-to-oneタグで作成されています。関連するエンティティのクラス名はtarget-entity属性で指定されていて、これはDBマッパーがほかのテーブルにアクセスするには十分な情報です。Bi-directionalなリレーションにおいてreporterとengineerは所有者側であるので、inversed-by属性も指定する必要があります。これはリレーションにおいて所有される側のフィールド名を指定する必要があります。次の例ではinversed-by属性はその対称物であるmapped-byがあることも見ていくことにします。 

 

最後はBug#productsコレクションを定義していきます。これは特定のバグが起きているすべてのプロダクトを意味しています。再び、target-entityとfield属性をmany-to-manyのタグで定義する必要があります。

 

最後にユーザエンティティを定義していきます

<?php
// src/User.php
/**
 * @Entity @Table(name="users")
 **/
class User
{
    /**
     * @Id @GeneratedValue @Column(type="integer")
     * @var int
     **/
    protected $id;

    /**
     * @Column(type="string")
     * @var string
     **/
    protected $name;

    /**
     * @OneToMany(targetEntity="Bug", mappedBy="reporter")
     * @var Bug[]
     **/
    protected $reportedBugs = null;

    /**
     * @OneToMany(targetEntity="Bug", mappedBy="engineer")
     * @var Bug[]
     **/
    protected $assignedBugs = null;

    // .. (other code)
}

 

ここでは新しくone-to-manyタグに言及していきます。そしてこれまでの所有側とその逆について思い出しておく必要があります。今、repotedBugsとassignedBugsは所有側とは逆にリレーションされています。これはjoinについての詳細はすでに所有側で定義されていることを示しています。

 

つまり、所有側であるBugクラスのプロパティを示してあげるだけでいいのです。

この例はmetadata definition languageの最も基本的な手法になります。

 

Databaseを更新します。

$ vendor/bin/doctrine orm:schema-tool:update --force

以下の様な出力結果が確認できました。

 

Updating database schema...

Database schema updated successfully! "7" queries were executed 

 

これによって、bugs, users, bug_productテーブルが出来たことが確認できました。

 

 

Implementing more Requirements

 

・さらなる要求を実装していきます

まず、create_userエンティティが必要です。

<?php
// create_user.php
require_once "bootstrap.php";

$newUsername = $argv[1];

$user = new User();
$user->setName($newUsername);

$entityManager->persist($user);
$entityManager->flush();

echo "Created User with ID " . $user->getId() . "\n";

 

次を実行します

$ php create_user.php beberlei

実行すると

PHP Fatal error:  Class 'ArrayCollection' not found in /Library/WebServer/Documents/doctrine-orm/src/User.php on line 33

errorを吐かれてしまいました。

 

さっきのコードで以下をなくしたことが原因でした。コードを消す時点でおかしいなと思っていたので、すぐ追加します。__construct()内で使ってるんだから当然ですよね。。。

use Doctrine\Common\Collections\ArrayCollection;

再度実行すると以下のように出力されbeberleiという国籍も想像できない人間がユーザに追加されます。。。

Created User with ID1

 

これで、バグを作成するデータができ、このシナリオは次のようになるでしょう。

<?php
// create_bug.php
require_once "bootstrap.php";

$theReporterId = $argv[1];
$theDefaultEngineerId = $argv[2];
$productIds = explode(",", $argv[3]);

$reporter = $entityManager->find("User", $theReporterId);
$engineer = $entityManager->find("User", $theDefaultEngineerId);
if (!$reporter || !$engineer) {
    echo "No reporter and/or engineer found for the input.\n";
    exit(1);
}

$bug = new Bug();
$bug->setDescription("Something does not work!");
$bug->setCreated(new DateTime("now"));
$bug->setStatus("OPEN");

foreach ($productIds as $productId) {
    $product = $entityManager->find("Product", $productId);
    $bug->assignToProduct($product);
}

$bug->setReporter($reporter);
$bug->setEngineer($engineer);

$entityManager->persist($bug);
$entityManager->flush();

echo "Your new Bug Id: ".$bug->getId()."\n";

 

ここではユーザとプロダクトは1つずつしかないので、次のようなスクリプトを実行してみます。

php create_bug.php 1 1 1

 

ここで初めてエンティティマネージャの呼び出しAPIを利用していて、EntityManager#find($name, $id)を呼び出し、プライマリキーによって、実行された1つのインスタンスが返されていることがわかります。

 

加えて、persistとflushパターンによってDBにBugが保存されていることも確認できます。

 

チュートリ(3)につづく