Doctrine 2 Gentleman's Kit for Symfony 3.3.6:エンティティの作成、関連付け、再帰的な関係





読者の皆さん、こんにちは!



記事を読んで、あなたと何をしますか







記事に興味がある人:



読者は、すでにSymfonyに精通している場合に興味があります-少なくとも1つの単純なプロジェクトがすでに完了しています。 高度な開発者の間でこの情報に関心があり、「斜めに」歩くことができました。 実際、この記事は私と一緒に働いている人たちのよくある質問に対する答えとして書かれています。 今、私は彼らにこの記事へのリンクを投げることができます。



エンティティ作成



Composerがシステムにインストールされていないかのように、すべてのコンソールコマンドを記述します。



Symfonyを起動するためにインストールします:

#             . php composer.phar create-project symfony/framework-standard-edition ./gentlemans_set "v3.3.6" #       cd gentlemans_set/ #     php bin/console doctrine:database:create
      
      







誰かが慣れていますが、自分で多くのコードを書くのは好きではありません。 Symfonyで何かの自動生成を使用できる場合、それを使用し、アドバイスします。これは人的要因の最小化であり、あなたの心を解放するだけです。 Symfonyコンソールコマンドを使用して2つの単純なエンティティを生成します。

 #   User php bin/console doctrine:generate:entity --entity=AppBundle:User --fields="username:string(127) password:string(127)" -q #   Product php bin/console doctrine:generate:entity --entity=AppBundle:Product --fields="name:string(127) description:text" -q
      
      





その結果、2つのエンティティクラスを作成しました。





そして、対応するエンティティのリポジトリの2つのクラス:





これらのエンティティのデータベース構造の作成に進む前に、エンティティ自体を検討します。 つまり、それらの注釈。 私の読者のほとんどはすでにこのトピックをよく理解していると思いますが、それでも、いくつかの点を説明します。



生成直後のエンティティsrc / AppBundle / Entity / Product.php:
 // ... /** * Product * * @ORM\Table(name="product") * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository") */ class Product { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string * * @ORM\Column(name="name", type="string", length=127) */ private $name; // ...
      
      







データベース構造を作成するために作成されるSQLクエリを確認します。

 php bin/console doctrine:schema:create --dump-sql CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(127) NOT NULL, password VARCHAR(127) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB; CREATE TABLE product (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(127) NOT NULL, description LONGTEXT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
      
      









エンティティ生成の結果、過剰な注釈が作成され、この段階で注釈をオプションに減らすと、エンティティはまったく同じ動作をします。



ブラッシュバージョン
 // src/AppBundle/Entity/Product.php // ... /** * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository") */ class Product { /** * @var int * * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string * * @ORM\Column(type="string", length=127) */ private $name; // ...
      
      





データベース構造を作成するために作成されるSQLクエリを確認し、まったく同じ結果を確認します。

 php bin/console doctrine:schema:create --dump-sql CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(127) NOT NULL, password VARCHAR(127) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB; CREATE TABLE product (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(127) NOT NULL, description LONGTEXT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
      
      









そのような例を引用するとき、私は何に注意を喚起したいですか? 圧倒的

ほとんどの場合、エンティティをモデルとして使用してデータベース構造を自動的に作成します。 このエンティティのテーブルに名前を付ける方法と各フィールドに名前を付ける方法を明示的に示した注釈のすべての部分を削除しました。 この構成データは、既存のデータベースを使用している場合にのみ必要であり、偶然にも、フィールド名が本質的にプロパティの名前に対応していません。 それらを指定しない場合、Doctrineはエンティティクラスの名前に従ってテーブルに名前を付け、エンティティのプロパティの名前に従ってフィールドに名前を付けます。 そして、これは正しいアプローチです。なぜなら、データベースのフィールドの名前はエンティティのプロパティと一致しており、これに影響を及ぼし、それを「驚かせる」ことができるサードパーティは存在しないからです。



しかし、あなたは抽象化のレベルを失い、各エンティティのリファクタリングでデータベース構造も更新する必要があると言うでしょう。 これに対する答えは次のとおりです。しかし、リファクタリングの結果として、エンティティのプロパティの名前とデータベース内のフィールドとの間の不正確な矛盾は、プロジェクトの技術的負債の貯金箱の余分なポイントです。



私は状況を誇張します:数年後、プロジェクトが世界規模に成長し、過剰な負荷に対処するためにサーバーのクラウド全体のデータベースをすでに投稿している場合、データベースに「this_may_work」という名前と「id」という名前のテーブルを見つけることができます、「foo」、「bar」、「some_field_2」。 名前が比較された本質においてより深い意味を持つという正当化は重要ではありません。



データベース構造の生成を開始します。



 php bin/console doctrine:schema:create
      
      







これで、データベースに作成されたテーブルにマップされた2つのエンティティができました。 インスタンスを作成し、データベースに保存してから、データベースから選択できます。 この記事でエンティティインスタンスの作成を実証するために、フィクスチャを使用することにし、エンティティリポジトリのメソッドでの選択を実証します。 エンティティリポジトリは既にありますが、フィクスチャデータを操作するメカニズムはまだありません。



フィクスチャをインストールする





doctrine / doctrine-fixtures-bundle依存関係をプロジェクトにドラッグします:

 php composer.phar require --dev doctrine/doctrine-fixtures-bundle
      
      







Symfonyコアの依存関係バンドルを接続します。

  // app/AppKernel.php // ... if (in_array($this->getEnvironment(), array('dev', 'test'))) { // ... $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(); } // ...
      
      







これでフィクスチャを作成する準備が整いました。 それらのディレクトリを作成します。



 mkdir -p src/AppBundle/DataFixtures/ORM
      
      







フィクスチャデータの初期ビュー:



src / AppBundle / DataFixtures / ORM / LoadCommonData.php
 <?php namespace AppBundle\DataFixtures\ORM; use AppBundle\Entity\Product; use AppBundle\Entity\User; use Doctrine\Common\DataFixtures\FixtureInterface; use Doctrine\Common\Persistence\ObjectManager; class LoadCommonData implements FixtureInterface { public function load(ObjectManager $manager) { $user = new User(); $user ->setPassword('some_password') ->setUsername(''); $manager->persist($user); $product = new Product(); $product ->setName('  ') ->setDescription('   .        .'); $manager->persist($product); $manager->flush(); } }
      
      









このコマンドで、このフィクスチャデータをデータベースにロードできます。



 php bin/console doctrine:fixtures:load
      
      







1対1の双方向通信





上記では、2つの無関係なエンティティを持つアプリケーションを取得しました。 これは、本質的に、このアプリケーションを完全に無意味にします。 実際のアプリケーションのデータのほとんどは、何らかの形で他のデータに関連しています。 ソーシャルネットワークのページ上のすべてのコメントは、ユーザーの本質に関連付けられています。そうでなければ、役に立たなくなります。



1対1の関係-多対多の関係よりも、プロジェクトでの頻度はさらに低いと感じています。 通常の形式に従う場合、これは自然なことです。 ただし、その実装方法を知る必要があります。



たとえば、売り手エンティティセラーを作成します。このユーザーが売り手である場合、エンティティユーザーと1対1でリンクされます。



 php bin/console doctrine:generate:entity --entity=AppBundle:Seller --fields="company:string(127) contacts:text" -q
      
      







次に、ユーザーエンティティを変更して、売り手エンティティとの接続を作成します。

src / AppBundle / Entity / User.php
 <?php // ... class User { // ... /** * @ORM\OneToOne(targetEntity="Seller") */ private $seller; // ... }
      
      









販売者の本質を変更して、ユーザーエンティティとの関係を追加します。

src / AppBundle / Entity / Seller.php
 <?php // ... class Seller { // ... /** * @ORM\OneToOne(targetEntity="User", mappedBy="seller") */ private $user; // ... }
      
      









すぐに双方向通信の例を挙げたことに注意してください...単方向通信を行う理由はまったくありません。 双方向通信はデータベースを変更しませんが、プログラムコードでは非常に便利です。 私の意見では、速度の最適化とRAMの使用の観点からも、主観的な意見は可能ですが、単方向通信では何も得られません。 (免責事項:再帰通信について説明している記事の最後のセクションでは、単方向通信の唯一の成功例が知られています。)



また、注釈を省略したことにも注意してください。 今回はアノテーション@JoinColumn。 これらの注釈は、以前に削除した注釈が必要だったのとまったく同じ目的で必要です。データベースでフィールドを作成する名前と、外部キーを作成するためにデータベースのフィールドを使用する名前を示します。 これはすべて、この注釈なしで可能な限り最良の方法で実行されます。



バンドルのすべてのエンティティでゲッター/セッターメソッドの自動生成を開始します。 これらのメソッドは、新しいエンティティプロパティ用に作成されます。

 php bin/console doctrine:generate:entities AppBundle
      
      







また、データベース構造をエンティティと一致させる必要があります。

 php bin/console doctrine:schema:update --force
      
      







データベース内に失いたくないデータがある場合は、最後のコマンドを注意して使用してください。 実動サーバーでは、このコマンドをまったく使用しないでください。 移行データベースとは何ですか?



さて、フィクスチャでは、売り手のエッセンスに既に接続しているもう1人のユーザーを作成します。

src / AppBundle / DataFixtures / ORM / LoadCommonData.php
 <?php namespace AppBundle\DataFixtures\ORM; use AppBundle\Entity\Product; use AppBundle\Entity\Seller; use AppBundle\Entity\User; use Doctrine\Common\DataFixtures\FixtureInterface; use Doctrine\Common\Persistence\ObjectManager; class LoadCommonData implements FixtureInterface { public function load(ObjectManager $manager) { $user = new User(); $user ->setPassword('some_password') ->setUsername(''); $manager->persist($user); $seller = new Seller(); $seller ->setCompany("  ") ->setContacts(" "); $manager->persist($seller); $seller_user = new User(); $seller_user ->setPassword('some_password') ->setUsername('') ->setSeller($seller); $manager->persist($seller_user); $product = new Product(); $product ->setName('  ') ->setDescription('   .        .'); $manager->persist($product); $manager->flush(); } }
      
      









フィクスチャデータのロードを開始し、データベースで作成されたレコードを賞賛できます。



これらの注釈がエンティティ間の関係を作成する方法を理解するのに問題があり、Doctrine 2の公式ドキュメントが役に立たない場合は、画像を見てください:







ここで、矢印は注釈に入力するためにどこから来たかを示します。



両方のエンティティの注釈は非常に類似していることに注意してください。 構文的には、 mappedBy 属性inversedBy 属性のみが異なります 。 しかし、この違いは基本です。 通常、inversedBy属性が存在するエンティティは、mappedBy属性を持つ従属エンティティと見なされます。 Userエンティティは、Sellerエンティティに従属していることがわかります。 これは、データベースでは、セラーテーブルへの外部キーを含むユーザーテーブルであり、その逆ではないという事実に表れています。 これは、フィクスチャデータで記述したコードにも影響します。売り手ではなく、setSellerメソッドを使用して売り手をユーザーに割り当てたことに注意してください。 後者のオプションは、データベースにまったく表示されません。 つまり、関連付けられている相手と示されているのは、下位エンティティのオブジェクトであることを理解する必要があります。



1対多の双方向通信





コミュニケーションの最も一般的な形式。 したがって、熱帯の島々のどこかにインターネットがなく、コンピューターも、手も、頭もなしに洞窟を通信するシステムが散らばっていない中毒状態でも、それを実現できる必要があります。 一般に、あなたの存在そのもので、1対多の関係をSymfonyプロジェクトに持ち込む必要があります。



上記のとおり、双方向通信の例を作成します。 これを行うには、既存のエンティティであるProductとSellerを接続します。



私たちはこの質問に答えます。だれがこの関係に多く参加し、誰が一人になるのか。 通常、1人の売り手から多くの製品が出されます。 したがって、新しいSellerエンティティプロパティには@ORM\OneToMany



があり、Productエンティティプロパティには@ORM\ManyToOne



ます。 それ以外の場合、すべては1対1接続タイプと同じです。 ただし、ここでは、「多くの」側のエンティティは常に「1」側のエンティティに従属するため、属性mappedByとinversedByを自由に交換することはできません。 したがって、製品のみが常にデータベースに外部キーを持ちます。 ロジックの継続:下位エンティティとしての商品に対して、売り手はsetSellerメソッドによって割り当てられます。このメソッドは、この予定がデータベースに保存されるように以下に記述します。



販売者のエッセンスを変更して、製品のエッセンスとの関係を作成します。

src / AppBundle / Entity / Seller.php
 <?php // ... class Seller { // ... /** * @ORM\OneToMany(targetEntity="Product", mappedBy="seller") */ private $products; // ... }
      
      









製品のエッセンスを変更し、売り手のエッセンスとの関係を追加します。

src / AppBundle / Entity / Product.php
 <?php // ... class Product { // ... /** * @ORM\ManyToOne(targetEntity="Seller", inversedBy="product") */ private $seller; // ... }
      
      









繰り返しますが、バンドルのすべてのエンティティでゲッター/セッターメソッドの自動生成を開始します。

また、データベース構造を再度更新するコマンドを実行します。 (チームが覚えていない場合は上記を参照)。



フィクスチャデータで既に作成されている製品に売り手を割り当てます。



 // src/AppBundle/DataFixtures/ORM/LoadCommonData.php // ... $product = new Product(); $product ->setSeller($seller) ->setName('  ') ->setDescription('   .        .'); // ...
      
      







フィクスチャデータのロードを開始すると、データベースで、製品レコード「Pillow for the Programmer」がSeller_idフィールドの値を取得したという事実に感心することができます。 これが私たちが求めていたものです。現在、当社の製品では、各売り手が多くの商品を所有できます。



コントローラーのアクションをチェックインするコード:

  // ... $product = $this->getDoctrine() ->getRepository("AppBundle:Product") ->find(6); dump($product->getSeller()->getCompany()); // ...
      
      







6は、執筆時点での製品のIDです(フィクスチャデータのロードを開始すると、データベーステーブルの古いレコードは削除されますが、主キーの自動インクリメントはリセットされません)。 ここでは、プライマリキー(idフィールド)の値によってリポジトリから製品を取得し、getSellerメソッドを使用して製品に関連付けられたセラーエンティティを取得しました。 出力は次のとおりです。

「角とひづめ」



次に、売り手のすべての製品を探してみましょう。 例として、コントローラーのアクションのコード:

  // ... $seller = $this->getDoctrine() ->getRepository("AppBundle:Seller") ->find(5); $names = []; foreach($seller->getProducts() as $product) { $names[] = $product->getName(); } dump($names); // ...
      
      







ダンプ機能の出力:

array:1 [▼

0 => " "

]









私に関しては、結果は達成されました。



双方向の多対多通信





このような関係がどのように構成されているかを知ることは非常に役立ちますが、1対多の関係ほど一般的ではありません。 データベース内の多対多のリレーションシップは、3番目のピボットテーブルを使用して2つのテーブルをリンクすることで構成されていることを既に知っているはずです。 この3番目のテーブルの本質は作成する必要はありません-データベース構造を作成または更新するコマンドを実行すると、Doctrineはこのテーブルを個別に作成します。



広大なプロジェクトでは、Categoryエンティティを作成し、その多対多の関係をProductに関連付けます。 したがって、1つの製品が一度に複数のカテゴリに属する​​機会を作成します。 ここでの「多対多」関係は簡単に明らかになります。多くの製品が1つのカテゴリに属し、1つの製品が多くのカテゴリに属する​​ことができます。製品側、カテゴリ側には「多」記号があり、多対多の関係が必要です。



カテゴリエンティティを作成します。

 php bin/console doctrine:generate:entity --entity=AppBundle:Category --fields="name:string(127)" -q
      
      







カテゴリーの本質を変更して、製品の本質との関係を作成します。

src / AppBundle / Entity / Category.php
 <?php // ... class Category { // ... /** * @ORM\ManyToMany(targetEntity="Product", mappedBy="categories") */ private $products; // ... }
      
      









製品の本質を変更し、カテゴリーの本質との関係を追加します。

src / AppBundle / Entity / Product.php
 <?php // ... class Product { // ... /** * @ORM\ManyToMany(targetEntity="Category", inversedBy="products") */ private $categories; // ... }
      
      









バンドルのすべてのエンティティでゲッター/セッターメソッドの自動生成を開始します。 また、データベース構造を再度更新するコマンドを実行します。 (チームが覚えていない場合は上記を参照)。



フィクスチャは2つのカテゴリの作成によって補完されます。別の製品を作成し、製品にカテゴリを割り当てます。 すべての製品がすべてのカテゴリにあることが判明しました。 神は私たちにマーケティングを許してくれましたが、これは明確にするためだけに行われました。



src / AppBundle / DataFixtures / ORM / LoadCommonData.php
 <?php namespace AppBundle\DataFixtures\ORM; use AppBundle\Entity\Category; use AppBundle\Entity\Product; use AppBundle\Entity\Seller; use AppBundle\Entity\User; use Doctrine\Common\DataFixtures\FixtureInterface; use Doctrine\Common\Persistence\ObjectManager; class LoadCommonData implements FixtureInterface { public function load(ObjectManager $manager) { $user = new User(); $user ->setPassword('some_password') ->setUsername(''); $manager->persist($user); $seller = new Seller(); $seller ->setCompany("  ") ->setContacts(" "); $manager->persist($seller); $seller_user = new User(); $seller_user ->setPassword('some_password') ->setUsername('') ->setSeller($seller); $manager->persist($seller_user); $category = new Category(); $category->setName(''); $manager->persist($category); $category2 = new Category(); $category2->setName(''); $manager->persist($category2); $product = new Product(); $product ->setSeller($seller) ->setName('  ') ->setDescription('   .        .'); $manager->persist($product); $product2 = new Product(); $product2 ->setSeller($seller) ->setName('  ') ->setDescription(',      64 ,   - 64 ,    16 '); $manager->persist($product2); $product->addCategory($category); $product->addCategory($category2); $product2->addCategory($category); $product2->addCategory($category2); $manager->flush(); } }
      
      









ここで重要なのは、接続に関係するエンティティが等しいにもかかわらず、 まだmappedBy属性とinversedBy属性を使用する必要があることです 。 これは予想外のことですが、「1対多」関係を作成するときに観察した動作はここで保持されます。mappedBy属性が指定された側のエンティティオブジェクトは、inversedBy属性が指定された側のエンティティオブジェクトに割り当てる必要があります そうでない場合、ピボットテーブルのエントリは表示されません。 意に反して、この点でどのエンティティが従属であり、2番目のエンティティのオブジェクトを割り当てるのはそのオブジェクトであるかを分離し、留意する必要があることがわかります。 この場合、従属エンティティはProductであり、Categoryオブジェクトをそのオブジェクトに割り当てます。 誰かが松葉杖なしでこれを回避する方法を知っている場合、注釈のみを変更します - コメントに書き込みます 。 私は通常、メインエンティティのセッターを少し修正して管理します(最近発見したように、Doctrine2の公式ドキュメントには同じ問題の解決策が記載されています ):



  // ... /** * Add product * * @param \AppBundle\Entity\Product $product * * @return Category */ public function addProduct(\AppBundle\Entity\Product $product) { $product->addCategory($this); //     ,      ,     $this->products[] = $product; return $this; } // ...
      
      







フィクスチャデータのデータベースへの読み込みを開始し、データベースをチェックして、product_categoryサマリーテーブルに4つのエントリが含まれていることを確認します。各エントリは、特定のカテゴリと特定の製品間の接続を確立します。







再帰接続



再帰接続は、それ自体がエンティティ上に構築されている場合に呼び出されます。 また、この接続は1対1、1対多、および多対多にできます。 ローミングする場所があります。 最初に、公式ドキュメントに記載されていないバリアントを見てみましょう。 「多対多」の単方向再帰接続です。



ユーザーの本質を変更し、自分と多対多の関係を追加します。

src / AppBundle / Entity / User.php
 <?php // ... class User { // ... /** * @ORM\ManyToMany(targetEntity="User") */ private $friends; // ... }
      
      









これは、単方向通信の正常な使用の例です。したがって、エンティティはピボットテーブルを介してプライマリキーによってそれ自体にマップされます。つまり、双方向性は不要です。 バンドルのエンティティの生成を開始し、データ構造を更新すると、次の行でフィクスチャーを補完できます。



 // ... public function load(ObjectManager $manager) { // ... $user->addFriend($seller_user); $seller_user->addFriend($user); $manager->flush(); } // ...
      
      







現在、私たちと一緒にいる各ユーザーは、できるだけ多くのユーザーフレンドを持つことができます。 これは、ツリー構造のように、主/従属、親/子の関係がない場合の非階層構造の優れた例です。



次に、再帰接続を作成してツリー構造を分析します。 カテゴリの本質を変更し、それ自体と1対多の関係を追加します。

src / AppBundle / Entity / Category.php
 <?php // ... class Category { // ... /** * @ORM\OneToMany(targetEntity="Category", mappedBy="parent") */ private $children; /** * @ORM\ManyToOne(targetEntity="Category", inversedBy="children") */ private $parent; // ... }
      
      









ここではすべてが明確であると思います-カテゴリには親と子孫があります。

データベース構造を更新し、ゲッター/セッターメソッドを生成した後、誰が誰の親で誰が誰の子孫であるかを示すフィクスチャデータを追加します。



 // ... public function load(ObjectManager $manager) { // ... $category2->setParent($category); $category->addChild($category2); $manager->flush(); } // ...
      
      







再帰的な1対1の関係は分析しません。これが必要な場合、類推して、自分でそれを書くことは問題ではないと思います。

複数の親と子孫が存在する可能性がある階層的な非ツリー構造の構築は、同じアナロジー方法で行われますが、問題はないと思います。



役に立つ



開発者の環境でアプリケーションを起動するときにページの下部に表示されるSymfonyツールバーを使用します:







エンティティのアノテーションを台無しにした場合、Doctrineはおそらくこれに気づき、ここでエラーを表示します。このアイコンをクリックすると、エラーに関する詳細を読むことができます。これにより、問題を解決できます。



道路上



彼はSignle Table Inheritanceと一緒に多相関係の話題に触れることを計画しましたが、記事はすでに法外に成長しました。それで、私はそれをすべて準備しておきます。私がどこかで台無しになったらコメントを書いてください。そのような量のテキストでは、目はとても石けんです。



ここに記事を書いた結果のプロジェクトを投稿します。しかし、エンティティ間の関係のトピックを研究しているだけの人には、プロジェクトを準備せずにすべての作業を自分で行うことをお勧めします。これは良いトレーニングです。この記事には、プロジェクトをゼロから作成するためのすべてが含まれています。








All Articles