OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

Symfony2で利用されているDoctrineに入門する(後編)

こんにちは!@mrtryです。
最近、つくりおきした鍋を冷蔵庫に入れ忘れて、おかずを腐らせる失態を2度ほど犯しています…。
みなさん…。最近暖かくなってきていますので、食中毒には気を付けましょう…。

さて、「Symfony2入門」の8回目の記事です。
前回のSymfony2で利用されているDoctrineに入門する(中編)に引き続き、今日は後編をお送りします。

前回は、DoctrineとDBを連携するための設定 エンティティの作成 エンティティを元したテーブル作成 を紹介しました。
今回は、 Doctrineを介したCRUD操作 について紹介したいと思います。

Create

Createする際の手順は以下のようになります。

  1. EntityManagerを準備する
  2. エンティティを新しく生成する
  3. EntityManagerを介して、生成したエンティティをDoctrineの管理下におく
  4. DBに永続化する

以下は、実際のコードの例です。

use AppBundle\Entity\Product;

public function createAction()
{
    // EntityManagerを準備する
    $em = $this->getDoctrine()->getManager();

    // 新しいエンティティを作成し、プロパティを設定する
    $product = new Product();
    $product->setName('Apple');
    $product->setPrice(100);
    $product->setDescription('From Aomori');

    // $productを永続化するエンティティとして管理する
    $em->persist($product);
    // DBへ永続化する
    $em->flush();

    ...
    
}

順に見ていきましょう。

    // EntityManagerを取得する  
    $em = $this->getDoctrine()->getManager();

EntityManagerというものが出てきました。 EntityManagerとは、エンティティの情報をDBへ永続化してくれるものです。 エンティティの情報をDBへ永続化する際は、このEntityManegerを介して行うことになります。

なお、今回はRDBとしてmysqlを用いているので、EntityManagerが返ってきますが、 MongoDBなどのドキュメント指向データベースを用いた際は、DocumentManagerが返ってきます。

    // 新しいエンティティを作成し、プロパティを設定する  
    $product = new Product();  
    $product->setName('Apple');  
    $product->setPrice(100);  
    $product->setDescription('From Aomori');  

前回生成したProductエンティティを新しく生成し、各セッターを用いてプロパティを設定しています。

    // $productを永続化するエンティティとして管理する  
    $em->persist($product);  
    // DBへ永続化する  
    $em->flush();

EntityManegerを介して、DBにエンティティの内容を反映します。 まず、

    $em->persist($product);

で、引数で渡されたエンティティを永続化するエンティティとしてDoctrineで管理します。 persistという名前を見て「これで永続化されるのかな?」と勘違いしてしまうかもしれませんが、 あくまで管理しているだけで、まだDBへの永続化処理は行われていません。

その後、

    $em->flush();

を行うと、それまでにpersist()されていたエンティティがDBへ永続化されます。

Read

Readする際の手順は、以下のようになります。

  1. 読み取りたいエンティティのリポジトリを準備する
  2. リポジトリのfind系メソッドを用いて、エンティティを取得する

以下は、実際のコードの例です。

public function readAction()
{
    // ProductRepositoryを取得
    $productRepository = $this->getDoctrine()->getRepository('AppBundle:Product');
  
    // productテーブルにあるカラム`name`について、`Apple`という文字列に一致するエンティティを取得する
    $product = $productRepository->findByName('Apple');
      
    ...
    
}

順に見ていきましょう。

    // ProductRepositoryを取得  
    $productRepository = $this->getDoctrine()->getRepository('AppBundle:Product');

今度は、ProductRepositoryというものが出てきました。 このリポジトリとは、DBのテーブルをオブジェクトとしたものです。 エンティティの永続化はEntityManagerを介して行いますが、 エンティティを取得するときは、このリポジトリを介して取得します。

    // productテーブルにあるカラム`name`について、`Apple`という文字列に一致するエンティティを取得する  
    $product = $productRepository->findByName('Apple');

$productRepositoryfindByName()を用いて、 productテーブルにあるカラムnameについて、Appleという文字列に一致するエンティティを取得します。

このように、エンティティを取得するは、Repositoryに実装されているfindXXXと名前についたメソッドを用いて取得します。

以下で、その他のfind系メソッドの例を紹介します

参考:Fetching Objects from the Database

// 主キーで検索し、一致するエンティティを取得する (普通はID)
$product = $repository->find($productId);

// 任意のカラム名に基づき、任意の値で検索し、該当するうちの最初のエンティティを取得
$product = $repository->findOneById($productId);
$product = $repository->findOneByName('Keyboard');

// 任意のカラム名に基づき、任意の値を検索し、一致するエンティティをすべて取得する
$products = $repository->findByPrice(19.99);

// すべてのエンティティを取得
$products = $repository->findAll();

Update

Updateする際の手順は、以下のようになります。

  1. リポジトリを介して、更新対象とするエンティティを取得する
  2. 取得したエンティティのプロパティを更新する
  3. DBへ永続化する

以下は、実際のコードの例です。

use AppBundle\Entity\Product;

public function updateAction()
{
    // EntityManagerを取得する
    $em = $this->getDoctrine()->getManager();

    $productId = XXX;

    // productテーブルからIDを指定してエンティティを取得する
    $product = $this->getDoctrine()
      ->getRepository('AppBundle:Product')
      ->find($productId);

    // priceに`100`をセットする
    $product->setPrice(100);

    // DBへ永続化する
    $em->flush();

    ...
    
}

CreateとReadで紹介したメソッドの組み合わせで実現できますが、1箇所だけ違うところがあります。 DBへの永続化の過程で、persist()が省略されています。 persist()はDoctrine側でこのエンティティを管理するよという意味で実行するものでした。 ですが、上記のコードのように、Repositoryを介して取得したエンティティについては、既にDoctrine側の管理対象となっているので、書かなくても良いです。

Notice that calling $em->persist($product) isn’t necessary. Recall that this method simply tells Doctrine to manage or “watch” the $product object. In this case, since you fetched the $product object from Doctrine, it’s already managed.

Databases and the Doctrine ORM (2.7) | updating-an-object

Delete

Updateする際の手順は、以下のようになります。

  1. リポジトリを介して削除対象とするエンティティを取得する。
  2. EntityManager->remove()を行い、削除するエンティティとして管理する
  3. DBへ永続化する

以下は、実際のコードの例です。

use AppBundle\Entity\Product;

public function deleteAction()
{
    // EntityManagerを取得する
    $em = $this->getDoctrine()->getManager();

    // productテーブルからIDを指定してエンティティを取得する
    $product = $this->getDoctrine()
      ->getRepository('AppBundle:Product')
      ->find($productId);

    //$productを削除するエンティティとして管理する
    $em->remove($product);
    // DBへ永続化する
    $em->flush();
}

エンティティを削除する際は、EntityManagerで用意されているremove()というメソッドを利用します。 remove()の引数となるエンティティは、実行したタイミングでDoctrineで管理されるので、flush()を行うと、DBへ永続化されます。

Doctrine Query Language(DQL)

複数のテーブルを組み合わせたクエリを発行する時、上記の手法だとなかなか難しいこともあります。 今回は詳しく紹介しませんが、DoctrineにはObject Query Language(OQL)として、Doctrine Query Language(DQL)というものも実装されています。

おわりに

今回は、DoctrineでCRUDをする例を紹介しました。
次回は、サービスコンテナについて紹介したいと思います。

また、この記事は、@mrtryの勉強の一環で書いていますので、 お気づきの点などがありましたら、コメント等でご指摘いただければ幸いです!

参考文献

Industry Tech Kaigi (4/27) の仲間に入れてもらってイベントやります!

@riaf です。

4/27 (木) に開催される#02 Industry Tech Kaigi「巨大産業をテクノロジーでハックせよ!」 というイベントにオトバンクも一緒に登壇することになりました。

公開時にちょっと面白いことになっていたので、シェアして遊んだりしていましたが、

現在はちゃんと修正されている通り、各社の開発部門の責任者が集まるパネルディスカッションをやるはずです。

詳細はイベントページをご覧いただければと思いますが、

industry-tech-kaigi.connpass.com

普段は共通点がなさすぎて集まることのないメンバーが集まりますので、面白くなるのかどうかも検討つきませんが、いつもとは違うお話ができると良いなあなどと思っております。

「こんな話が聞きたい!」といったリクエストは @frkout まで送ってください!

Symfony2で利用されているDoctrineに入門する(中編)

こんにちわ!@mrtryです。 もう3月ですね。新卒入社して、もうすぐ1年経ちます。 新卒と言えなくなってしまうのが、ちょっと寂しい今日このごろです。

さて、「Symfony2入門」の7回目の記事です。 前回のSymfony2で利用されているDoctrineに入門する(前編)に引き続き、今日は中編をお送りします。

前回は、Doctrineについてざっくり紹介しました。 今回は、DoctrineとDBを連携するための設定 エンティティの作成 エンティティを元したテーブル作成 を紹介したいと思います。

プロジェクトをつくる

まず、準備として、Symfonyのプロジェクトをつくります。 今回はtry-doctrineという名前で作成します。

$ symfony new try-doctrine 2.8

 Downloading Symfony...

    5.29 MB/5.29 MB ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  100%

 Preparing project...

 ✔  Symfony 2.8.15 was successfully installed. Now you can:

    * Change your current directory to /Users/symmt/Program/Study/try-doctrine

    * Configure your application in app/config/parameters.yml file.

    * Run your application:
        1. Execute the php app/console server:run command.
        2. Browse to the http://localhost:8000 URL.

    * Read the documentation at http://symfony.com/doc

RDBの設定

エンティティを生成する前に、DBの設定を行います。 今回は、RDBとしてmysqlを用いて実例を紹介します。

まず、app/config/parameters.ymlにて、設定を行います。 このファイルは、Symfonyのインフラに関係する設定をするためのファイルです。 各Bundleの設定や、APIのURLなどの定数の管理などに利用されます。

プロジェクトを立ち上げた直後のparameters.ymlにはDBとメールサーバーの設定が記述されています。 今回はdatabase_nameの箇所をtry-doctrineと書き換えたものを利用します。

$ cat -n app/config/parameters.yml
  1 # This file is auto-generated during the composer install
  2 parameters:
  3     database_host: 127.0.0.1
  4     database_port: null
  5     database_name: try-doctrine
  6     database_user: root
  7     database_password: null
  8     mailer_transport: smtp
  9     mailer_host: 127.0.0.1
 10     mailer_user: null
 11     mailer_password: null
 12     secret: 328b4f9d66009015aa9e8770eb73163d0b74684f

設定が終わったら、doctrine:database:createコマンドを実行します。 実行すると、database_nameで設定した名前でDBが新しく作成されます。

$ app/console doctrine:database:create
Created database `try-doctrine` for connection named default

実際に一覧を見てみると、DBが作成されていることが確認できます。

mysql> show databases;
+---------------------------+
| Database                  |
+---------------------------+
| ...                       |
| try-doctrine              |
+---------------------------+
11 rows in set (0.01 sec)

エンティティを生成する

次に、エンティティを生成します。 doctrine:generate:entityというコマンドを利用します。 実行すると対話形式となり、一通り入力が完了すると、エンティティが生成されます。

今回は例として、Productというエンティティを生成したいと思います。 フィールド名等については、以下の表をご確認ください。

フィールド名 null許容 主キー
id id false true
name string(255) false false
price integer false false
description text false false

実際に生成する

上記の表に沿って、エンティティを生成した際の様子です。

$ app/console doctrine:generate:entity


  Welcome to the Doctrine2 entity generator



This command helps you generate Doctrine2 entities.

First, you need to give the entity name you want to generate.
You must use the shortcut notation like AcmeBlogBundle:Post.

The Entity shortcut name: AppBundle:Product

Determine the format to use for the mapping information.

Configuration format (yml, xml, php, or annotation) [annotation]:

Instead of starting with a blank entity, you can add some fields now.
Note that the primary key will be added automatically (named id).

Available types: array, simple_array, json_array, object,
boolean, integer, smallint, bigint, string, text, datetime, datetimetz,
date, time, decimal, float, binary, blob, guid.

New field name (press <return> to stop adding fields): name
Field type [string]:
Field length [255]:
Is nullable [false]:
Unique [false]:

New field name (press <return> to stop adding fields): price
Field type [string]: integer
Is nullable [false]:
Unique [false]:

New field name (press <return> to stop adding fields): description
Field type [string]: text
Is nullable [false]:
Unique [false]:

New field name (press <return> to stop adding fields):


  Entity generation


  created ./src/AppBundle/Entity/Product.php
> Generating entity class src/AppBundle/Entity/Product.php: OK!
> Generating repository class src/AppBundle/Repository/ProductRepository.php: OK!


  Everything is OK! Now get to work :).

各項目について

First, you need to give the entity name you want to generate.
You must use the shortcut notation like AcmeBlogBundle:Post.

The Entity shortcut name: AppBundle:Product

生成するエンティティ名とそれを置くBundle先をBundle名:エンティティ名という書き方で指定します。 今回はAppBundleProductというエンティティを生成するので、AppBundle:Productと書きました。

Determine the format to use for the mapping information.

Configuration format (yml, xml, php, or annotation) [annotation]:

マッピング定義のフォーマットを指定します。 マッピング定義とはアプリケーション内のクラス、プロパティと、DBのテーブル、カラムの対応関係を定義しているものです。 ざっくりなイメージとしては、 ER図の内容を設定ファイルとしたものといった感じです。 デフォルトでは、annotationで設定するようになっています。 今回はannotationでよかったので、そのままReturnで進めました。

Instead of starting with a blank entity, you can add some fields now.
Note that the primary key will be added automatically (named id).

IDの項目はDoctrine側で自動的に生成されます。 今回で言うと、idがそれにあたります。

Available types: array, simple_array, json_array, object,
boolean, integer, smallint, bigint, string, text, datetime, datetimetz,
date, time, decimal, float, binary, blob, guid.

New field name (press <return> to stop adding fields): name
Field type [string]:
Field length [255]:
Is nullable [false]:
Unique [false]:

New field name (press <return> to stop adding fields): price
Field type [string]: integer
Is nullable [false]:
Unique [false]:

New field name (press <return> to stop adding fields): description
Field type [string]: text
Is nullable [false]:
Unique [false]:

フィールド名とその型を設定していきます。 型は、Available typesにて列挙されているものを指定できます。

フィールド名 null許容 ユニークキー制約
name string false false
price integer false false
description text false false

入力が終わったら、New field name (press <return> to stop adding fields)と書いてあるとおり、フィールド名を入力せずReturnを押すと、次に進みます。

  Entity generation


  created ./src/AppBundle/Entity/Product.php
> Generating entity class src/AppBundle/Entity/Product.php: OK!
> Generating repository class src/AppBundle/Repository/ProductRepository.php: OK!


  Everything is OK! Now get to work :).

ここまで終えると、エンティティとリポジトリが生成されます。 生成されたエンティティは以下のようになります。 先程設定していたフィールドがプロパティとして書かれ、プロパティの上部には設定した型などの情報がannotationで書かれていることが確認できます。 (突然出てきたリポジトリですが、今回は置いときます)

$ cat -n src/AppBundle/Entity/Product.php
  1  <?php
  2
  3  namespace AppBundle\Entity;
  4
  5  use Doctrine\ORM\Mapping as ORM;
  6
  7  /**
  8   * Product
  9   *
 10   * @ORM\Table(name="product")
 11   * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
 12   */
 13  class Product
 14  {
 15      /**
 16       * @var int
 17       *
 18       * @ORM\Column(name="id", type="integer")
 19       * @ORM\Id
 20       * @ORM\GeneratedValue(strategy="AUTO")
 21       */
 22      private $id;
 23
 24      /**
 25       * @var string
 26       *
 27       * @ORM\Column(name="name", type="string", length=255)
 28       */
 29      private $name;
 30
 31      /**
 32       * @var int
 33       *
 34       * @ORM\Column(name="price", type="integer")
 35       */
 36      private $price;
 37
 38      /**
 39       * @var string
 40       *
 41       * @ORM\Column(name="description", type="text")
 42       */
 43      private $description;
 44
 45
 46      /**
 47       * Get id
 48       *
 49       * @return integer
 50       */
 51      public function getId()
 52      {
 53          return $this->id;
 54      }
 55
 56      /**
 57       * Set name
 58       *
 59       * @param string $name
 60       * @return Product
 61       */
 62      public function setName($name)
 63      {
 64          $this->name = $name;
 65
 66          return $this;
 67      }
 68
 69      /**
 70       * Get name
 71       *
 72       * @return string
 73       */
 74      public function getName()
 75      {
 76          return $this->name;
 77      }
 78
 79      /**
 80       * Set price
 81       *
 82       * @param integer $price
 83       * @return Product
 84       */
 85      public function setPrice($price)
 86      {
 87          $this->price = $price;
 88
 89          return $this;
 90      }
 91
 92      /**
 93       * Get price
 94       *
 95       * @return integer
 96       */
 97      public function getPrice()
 98      {
 99          return $this->price;
100      }
101
102      /**
103       * Set description
104       *
105       * @param string $description
106       * @return Product
107       */
108      public function setDescription($description)
109      {
110          $this->description = $description;
111
112          return $this;
113      }
114
115      /**
116       * Get description
117       *
118       * @return string
119       */
120      public function getDescription()
121      {
122          return $this->description;
123      }
124  }

生成したエンティティをDBに反映する

Productエンティティを作成しましたが、それに対応するproductテーブルはまだDBに存在していません。 Productエンティティに対応するproductテーブルを作成する必要があります。

既に作成されているエンティティを元にして、DBのスキーマを更新するというdoctrine:schema:updateコマンドがあります。 これを実行して、Productエンティティに対応するproductテーブルを生成します。

app/console doctrine:schema:update
ATTENTION: This operation should not be executed in a production environment.
           Use the incremental update to detect changes during development and use
           the SQL DDL provided to manually update your database in production.

The Schema-Tool would execute "1" queries to update the database.
Please run the operation by passing one - or both - of the following options:
    doctrine:schema:update --force to execute the command
    doctrine:schema:update --dump-sql to dump the SQL statements to the screen

doctrine:schema:update --dump-sql を実行すると、実際に実行されるSQLを確認することができます。 doctrine:schema:update --force を実行すると、--dump-sqlで確認したSQLが実行されます。

実際にやっていきます。 try-doctrineに存在するテーブルを確認します。 まだ何も作成していないので、Emptyになっています。

mysql> show tables;
Empty set (0.00 sec)

doctrine:schema:update --dump-sqlを実行します。 先程生成したエンティティと同等の内容のテーブルを生成するSQLが発行されています。 doctrine:schema:update --forceを実行すると、このSQLが実行されることになります。

$ app/console doctrine:schema:update --dump-sql
CREATE TABLE product (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, price INT NOT NULL, description LONGTEXT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;

実際に実行されるSQLを確認したところで、doctrine:schema:update --forceを実行して、テーブルを作成します。

app/console doctrine:schema:update --force
Updating database schema...
Database schema updated successfully! "1" queries were executed

try-doctrineに存在するテーブルを確認します。 productテーブルが新規に作成され、フィールド名等の設定もエンティティの設定と同等なものになっています。

mysql> show tables;
+------------------------+
| Tables_in_try-doctrine |
+------------------------+
| product                |
+------------------------+
1 row in set (0.00 sec)

mysql> show columns from product;
+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | int(11)      | NO   | PRI | NULL    | auto_increment |
| name        | varchar(255) | NO   |     | NULL    |                |
| price       | int(11)      | NO   |     | NULL    |                |
| description | longtext     | NO   |     | NULL    |                |
+-------------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

これで、PHPでDoctrineを介してProductエンティティを操作することで、DBを操作する準備が整いました。

おわりに

今回は DoctrineとDBを連携するための設定 エンティティの作成 エンティティを元したテーブル作成 を紹介しました。 次回は、DoctrineでCRUDをする具体例を紹介したいと思います。

また、この記事は、@mrtryの勉強の一環で書いていますので、
お気づきの点などがありましたら、コメント等でご指摘いただければ幸いです!

参考