モンモンブログ

技術的な話など

(Symfony2/Doctrine2) Entity の OneToMany のアノテーションを自動生成させる

Symfony2 で、Entity の OneToMany アノテーションを自動生成させる方法についてです。Doctrineのライブラリに手を加えて実現します。
対象バージョンは Doctrine2.3.2。
じゃーいくよ。

まず基本から。Symfony2 での Entity の生成の仕方はこう。

  1. DBのスキーマ情報をXMLに落とす(このXML作っても無視されるみたいだけど…)

    app/console doctrine:mapping:convert xml src/Monmon/HmmBundle/Resources/config/doctrine/metadata/orm --from-database --force

  2. Entity を生成。DBのカラムに対応するフィールドのみ。

    app/console doctrine:mapping:import MonmonHmmBundle annotation

  3. Entity の中身のフィールドだけ見て、getter / setter をセットする。

    app/console doctrine:generate:entities MonmonHmmBundle

外部キーが張ってあるテーブルについては、参照先のエンティティを指すフィールドも作ってくれます。 ただし FROM テーブルから TO テーブルの参照(ManyToOne)だけ。逆の参照(OneToMany)はなし。

例。テーブル構造がこんなだとすると、

CREATE TABLE `category` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(128) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `product` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `category_id` int(11) NOT NULL,
  `name` varchar(128) NOT NULL,
  `price` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `category_id` (`category_id`),
  CONSTRAINT `product_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Entityはこんな感じ。Product の方に $category フィールドが出来てるのに注目。

// src/Monmon/HmmBundle/Entity/Category.php
/**
 * Category
 *
 * @ORM\Table(name="category")
 * @ORM\Entity
 */
class Category
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=128, nullable=false)
     */
    private $name;

    // 以下略
}

// src/Monmon/HmmBundle/Entity/Product.php

/**
 * Product
 *
 * @ORM\Table(name="product")
 * @ORM\Entity
 */
class Product
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=128, nullable=false)
     */
    private $name;

    /**
     * @var integer
     *
     * @ORM\Column(name="price", type="integer", nullable=false)
     */
    private $price;

    /**
     * @var \Category
     *
     * @ORM\ManyToOne(targetEntity="Category")        // ←「ManyToOne」アノテーションで表現
     * @ORM\JoinColumns({
     *   @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     * })
     */
    private $category;                  // ←Category Entity への参照ができてる!

    // 以下略
}

でもこれだけだと、こういうことはできるけど、

{{ product.category.name }}

これが出来ないよね。

{% for product in category.products %}
  {{ product.name }}<br>
{% endfor %}

でもこれやりたい場面ってしょっちゅうあるじゃん。

なんでこれ自動生成してくれないんだー?
$products フィールド作って ManyToOne アノテーションつけといてくれよー。
どこかにやり方はないのかー?
って探してたら、ありました。

php - Symfony2 Doctrine2 - generate One-To-Many annotation from existing database by doctrine:mapping:import - Stack Overflow

Doctrine のライブラリそのものを書き換えてしまいます。抵抗ある人は回れ右。修正するファイルは↓こちら。

vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php

ですが、これにはちょっとしたバグがありました。
あるテーブルから1つのテーブルに対し2つ以上の外部キーを張ってると、「フィールドが重複してんぞー」ってエラー吐かれて生成出来ません。
それってどんな場合かって?たとえばこんな。

CREATE TABLE `city` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(128) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `distance` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `city1_id` int(11) NOT NULL,
  `city2_id` int(11) NOT NULL,
  `price` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `city1_id` (`city1_id`),
  KEY `city2_id` (`city2_id`),
  CONSTRAINT `distance_ibfk_1` FOREIGN KEY (`city1_id`) REFERENCES `city` (`id`),
  CONSTRAINT `distance_ibfk_2` FOREIGN KEY (`city2_id`) REFERENCES `city` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

都市間の距離を表す distance テーブルに city テーブルへの参照が2つあるため、 City エンティティに同名の $distance フィールドを2つ載せようとして失敗します。

$ app/console doctrine:mapping:import MonmonHmmBundle annotation


  [Doctrine\ORM\Mapping\MappingException]
  Property "distance" in "City" was already declared, but it must be declared only once

こういう場合ってどうするのが正しいんでしょうねー?
$distances1, $distances2 って連番にする?
僕はめんどくさかったので「同じテーブルから複数外部キーが張られてる場合は OneToMany 参照は持たせない!」てことにしちゃいました。

パッチはこちら。 https://github.com/monmonmon/autogen-onetomany-annotations

symfony2 のプロジェクトトップでパッチをあててね。

$ patch -p0 < patch.txt
patching file vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php

で、Entityを再生成してみると、
てってれー。Category に $product フィールドが追加されました。

/**
 * Category
 *
 * @ORM\Table(name="category")
 * @ORM\Entity
 */
class Category
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=128, nullable=false)
     */
    private $name;

    /**
     * @var \Doctrine\Common\Collections\Collection
     *
     * @ORM\OneToMany(targetEntity="Product", mappedBy="category")
     */
    private $product;    // ( ´ ▽ ` )ノ

    /**
     * Constructor
     */
    public function __construct()
    {
        // コンストラクタも追加されます
        $this->product = new \Doctrine\Common\Collections\ArrayCollection();
    }

    // 以下略

複数形 $products にしたいとこですけど。あとで直すかも。

じゃね。