How to Work with Doctrine Associations / Relations

Screencast

スクリーンキャスト

Do you prefer video tutorials? Check out the Mastering Doctrine Relations screencast series.

ビデオチュートリアルが好きですか? Mastering Doctrine Relationsscreencast シリーズをチェックしてください。

There are two main relationship/association types:

主な関係/関連付けのタイプは 2 つあります。
ManyToOne / OneToMany
The most common relationship, mapped in the database with a foreign key column (e.g. a category_id column on the product table). This is actually only one association type, but seen from the two different sides of the relation.
データベースで外部キー列 (例: product テーブルの category_id 列) にマップされる最も一般的な関係。これは実際には 1 つの関連付けタイプにすぎませんが、関係の 2 つの異なる側面から見られます。
ManyToMany
Uses a join table and is needed when both sides of the relationship can have many of the other side (e.g. "students" and "classes": each student is in many classes, and each class has many students).
結合テーブルを使用し、リレーションシップの両側が反対側の多くを持つことができる場合に必要です (たとえば、「学生」と「クラス」: 各学生は多くのクラスに属し、各クラスには多くの学生がいます)。

First, you need to determine which relationship to use. If both sides of the relation will contain many of the other side (e.g. "students" and "classes"), you need a ManyToMany relation. Otherwise, you likely need a ManyToOne.

まず、使用する関係を決定する必要があります。リレーションの両側に他の側の多くが含まれる場合 (例: 「生徒」と「クラス」)、ManyToMany リレーションが必要です。それ以外の場合は、ManyToOne が必要になる可能性があります。

Tip

ヒント

There is also a OneToOne relationship (e.g. one User has one Profile and vice versa). In practice, using this is similar to ManyToOne.

1 対 1 の関係もあります (たとえば、1 人のユーザーが 1 つのプロファイルを持ち、その逆)。実際には、これを使用することは ManyToOne に似ています。

The ManyToOne / OneToMany Association

Suppose that each product in your application belongs to exactly one category. In this case, you'll need a Category class, and a way to relate a Product object to a Category object.

アプリケーション内の各製品が正確に 1 つのカテゴリに属しているとします。この場合、Category クラスと、Product オブジェクトを Category オブジェクトに関連付ける方法が必要になります。

Start by creating a Category entity with a name field:

まず、名前フィールドを持つカテゴリ エンティティを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ php bin/console make:entity Category

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

Field type (enter ? to see all types) [string]:
> string

Field length [255]:
> 255

Can this field be null in the database (nullable) (yes/no) [no]:
> no

New property name (press <return> to stop adding fields):
>
(press enter again to finish)

This will generate your new entity class:

これにより、新しいエンティティ クラスが生成されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Entity/Category.php
namespace App\Entity;

// ...

#[ORM\Entity(repositoryClass: CategoryRepository::class)]
class Category
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private $id;

    #[ORM\Column]
    private string $name;

    // ... getters and setters
}

Mapping the ManyToOne Relationship

In this example, each category can be associated with many products. But, each product can be associated with only one category. This relationship can be summarized as: many products to one category (or equivalently, one category to many products).

この例では、各カテゴリを多くの製品に関連付けることができます。ただし、各製品は 1 つのカテゴリにのみ関連付けることができます。この関係は、次のように要約できます。多くの製品と 1 つのカテゴリ (または同等の 1 つのカテゴリと多くの製品)。

From the perspective of the Product entity, this is a many-to-one relationship. From the perspective of the Category entity, this is a one-to-many relationship.

Product エンティティから見ると、これは多対 1 の関係です。Category エンティティから見ると、これは 1 対多の関係です。

To map this, first create a category property on the Product class with the ManyToOne annotation. You can do this by hand, or by using the make:entity command, which will ask you several questions about your relationship. If you're not sure of the answer, don't worry! You can always change the settings later:

これをマッピングするには、まず、ManyToOne アノテーションを使用して Product クラスにカテゴリ プロパティを作成します。これは手動で行うことも、make:entity コマンドを使用して行うこともできます。これにより、関係についていくつか質問されます。答えがわからない場合でも、心配しないでください。設定は後でいつでも変更できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ php bin/console make:entity

Class name of the entity to create or update (e.g. BraveChef):
> Product

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

Field type (enter ? to see all types) [string]:
> relation

What class should this entity be related to?:
> Category

Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
> ManyToOne

Is the Product.category property allowed to be null (nullable)? (yes/no) [yes]:
> no

Do you want to add a new property to Category so that you can access/update
Product objects from it - e.g. $category->getProducts()? (yes/no) [yes]:
> yes

New field name inside Category [products]:
> products

Do you want to automatically delete orphaned App\Entity\Product objects
(orphanRemoval)? (yes/no) [no]:
> no

New property name (press <return> to stop adding fields):
>
(press enter again to finish)

This made changes to two entities. First, it added a new category property to the Product entity (and getter & setter methods):

これにより、2 つのエンティティが変更されました。まず、新しいカテゴリ プロパティを Product エンティティ (およびゲッター メソッドとセッター メソッド) に追加しました。
  • Attributes
    属性
  • YAML
    YAML
  • XML
    XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Entity/Product.php
namespace App\Entity;

// ...
class Product
{
    // ...

    #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'products')]
    private $category;

    public function getCategory(): ?Category
    {
        return $this->category;
    }

    public function setCategory(?Category $category): self
    {
        $this->category = $category;

        return $this;
    }
}

This ManyToOne mapping is required. It tells Doctrine to use the category_id column on the product table to relate each record in that table with a record in the category table.

この ManyToOne マッピングは必須です。商品テーブルの category_id 列を使用して、そのテーブルの各レコードをカテゴリ テーブルのレコードと関連付けるよう Doctrine に指示します。

Next, since one Category object will relate to many Product objects, the make:entity command also added a products property to the Category class that will hold these objects:

次に、1 つの Category オブジェクトが多くの Product オブジェクトに関連するため、make:entity コマンドは、これらのオブジェクトを保持する Categoryclass に products プロパティも追加しました。
  • Attributes
    属性
  • YAML
    YAML
  • XML
    XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/Entity/Category.php
namespace App\Entity;

// ...
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

class Category
{
    // ...

    #[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category')]
    private $products;

    public function __construct()
    {
        $this->products = new ArrayCollection();
    }

    /**
     * @return Collection|Product[]
     */
    public function getProducts(): Collection
    {
        return $this->products;
    }

    // addProduct() and removeProduct() were also added
}

The ManyToOne mapping shown earlier is required, But, this OneToMany is optional: only add it if you want to be able to access the products that are related to a category (this is one of the questions make:entity asks you). In this example, it will be useful to be able to call $category->getProducts(). If you don't want it, then you also don't need the inversedBy or mappedBy config.

前に示した ManyToOne マッピングは必須ですが、この OneToMany はオプションです: カテゴリに関連する製品にアクセスできるようにしたい場合にのみ追加してください (これは make:entity が尋ねる質問の 1 つです)。この例では、$category->getProducts() を呼び出すことができると便利です。それが必要ない場合は、inversedBy または MappedByconfig も必要ありません。
ArrayCollection とは何ですか?

The code inside __construct() is important: The $products property must be a collection object that implements Doctrine's Collection interface. In this case, an ArrayCollection object is used. This looks and acts almost exactly like an array, but has some added flexibility. Just imagine that it is an array and you'll be in good shape.

__construct() 内のコードは重要です: $products プロパティは、Doctrine の Collection インターフェースを実装するコレクション オブジェクトでなければなりません。この場合、ArrayCollection オブジェクトが使用されます。これは配列とほとんど同じように見え、機能しますが、柔軟性がいくらか追加されています。それが配列であると想像してみてください。そうすれば、良い状態になるでしょう。

Your database is set up! Now, run the migrations like normal:

データベースがセットアップされました。次に、通常どおり移行を実行します。
1
2
$ php bin/console doctrine:migrations:diff
$ php bin/console doctrine:migrations:migrate

Thanks to the relationship, this creates a category_id foreign key column on the product table. Doctrine is ready to persist our relationship!

この関係のおかげで、これにより、product テーブルに category_id 外部キー列が作成されます。 Doctrine は私たちの関係を維持する準備ができています!

Now you can see this new code in action! Imagine you're inside a controller:

これで、この新しいコードの動作を確認できます!コントローラーの中にいると想像してください:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// src/Controller/ProductController.php
namespace App\Controller;

// ...
use App\Entity\Category;
use App\Entity\Product;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class ProductController extends AbstractController
{
    #[Route('/product', name: 'product')]
    public function index(ManagerRegistry $doctrine): Response
    {
        $category = new Category();
        $category->setName('Computer Peripherals');

        $product = new Product();
        $product->setName('Keyboard');
        $product->setPrice(19.99);
        $product->setDescription('Ergonomic and stylish!');

        // relates this product to the category
        $product->setCategory($category);

        $entityManager = $doctrine->getManager();
        $entityManager->persist($category);
        $entityManager->persist($product);
        $entityManager->flush();

        return new Response(
            'Saved new product with id: '.$product->getId()
            .' and new category with id: '.$category->getId()
        );
    }
}

When you go to /product, a single row is added to both the category and product tables. The product.category_id column for the new product is set to whatever the id is of the new category. Doctrine manages the persistence of this relationship for you:

/product に移動すると、1 つの行がカテゴリ テーブルと製品テーブルの両方に追加されます。新しい製品の product.category_id 列は、新しいカテゴリの ID に設定されます。 Doctrine は、この関係の持続性を管理します:

If you're new to an ORM, this is the hardest concept: you need to stop thinking about your database, and instead only think about your objects. Instead of setting the category's integer id onto Product, you set the entire Category object. Doctrine takes care of the rest when saving.

ORM を初めて使用する場合、これは最も難しい概念です。データベースについて考えるのをやめて、代わりにオブジェクトについて考える必要があります。カテゴリの整数 ID を Product に設定する代わりに、Category オブジェクト全体を設定します。Doctrine は保存時に残りを処理します。
逆側から関係を更新する

Could you also call $category->addProduct() to change the relationship? Yes, but, only because the make:entity command helped us. For more details, see: associations-inverse-side.

$category->addProduct() を呼び出して関係を変更することもできますか?はい。ただし、make:entity コマンドが役に立ったからです。詳細については、associations-inverse-side を参照してください。

When you need to fetch associated objects, your workflow looks like it did before. First, fetch a $product object and then access its related Category object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Controller/ProductController.php
namespace App\Controller;

use App\Entity\Product;
// ...

class ProductController extends AbstractController
{
    public function show(ManagerRegistry $doctrine, int $id): Response
    {
        $product = $doctrine->getRepository(Product::class)->find($id);
        // ...

        $categoryName = $product->getCategory()->getName();

        // ...
    }
}

In this example, you first query for a Product object based on the product's id. This issues a query to fetch only the product data and hydrates the $product. Later, when you call $product->getCategory()->getName(), Doctrine silently makes a second query to find the Category that's related to this Product. It prepares the $category object and returns it to you.

この例では、最初に製品の sid に基づいて製品オブジェクトを照会します。これにより、商品データのみを取得するクエリが発行され、$product がハイドレートされます。後で $product->getCategory()->getName() を呼び出すと、Doctrine は黙って 2 番目のクエリを作成し、この製品に関連するカテゴリを見つけます。 $category オブジェクトを準備し、それを返します。

What's important is the fact that you have access to the product's related category, but the category data isn't actually retrieved until you ask for the category (i.e. it's "lazily loaded").

重要なのは、製品の関連カテゴリにアクセスできるという事実ですが、カテゴリ データは、カテゴリを要求するまで実際には取得されません (つまり、「遅延ロード」されます)。

Because we mapped the optional OneToMany side, you can also query in the other direction:

オプションの OneToMany 側をマップしたため、他の方向でクエリを実行することもできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Controller/ProductController.php

// ...
class ProductController extends AbstractController
{
    public function showProducts(ManagerRegistry $doctrine, int $id): Response
    {
        $category = $doctrine->getRepository(Category::class)->find($id);

        $products = $category->getProducts();

        // ...
    }
}

In this case, the same things occur: you first query for a single Category object. Then, only when (and if) you access the products, Doctrine makes a second query to retrieve the related Product objects. This extra query can be avoided by adding JOINs.

この場合、同じことが起こります。まず、1 つの Category オブジェクトに対してクエリを実行します。次に、製品にアクセスする場合にのみ、Doctrine は関連する製品オブジェクトを取得するために 2 番目のクエリを作成します。この余分なクエリは、JOIN を追加することで回避できます。
リレーションシップとプロキシ クラス

This "lazy loading" is possible because, when necessary, Doctrine returns a "proxy" object in place of the true object. Look again at the above example:

この「遅延読み込み」が可能なのは、Doctrine が必要に応じて真のオブジェクトの代わりに「プロキシ」オブジェクトを返すためです。上記の例をもう一度見てください。
1
2
3
4
5
6
7
$product = $doctrine->getRepository(Product::class)->find($id);

$category = $product->getCategory();

// prints "Proxies\AppEntityCategoryProxy"
dump(get_class($category));
die();

This proxy object extends the true Category object, and looks and acts exactly like it. The difference is that, by using a proxy object, Doctrine can delay querying for the real Category data until you actually need that data (e.g. until you call $category->getName()).

このプロキシ オブジェクトは真の Category オブジェクトを拡張し、見た目も動作もそれとまったく同じです。違いは、プロキシ オブジェクトを使用することで、実際にそのデータが必要になるまで (たとえば、$category->getName() を呼び出すまで)、Doctrine は実際のカテゴリ データのクエリを遅らせることができることです。

The proxy classes are generated by Doctrine and stored in the cache directory. You'll probably never even notice that your $category object is actually a proxy object.

プロキシ クラスは Doctrine によって生成され、キャッシュ ディレクトリに格納されます。おそらく、$category オブジェクトが実際にはプロキシ オブジェクトであることに気付かないでしょう。

In the next section, when you retrieve the product and category data all at once (via a join), Doctrine will return the true Category object, since nothing needs to be lazily loaded.

次のセクションでは、製品とカテゴリのデータを一度に (結合を介して) 取得すると、何も遅延ロードする必要がないため、Doctrine は真の Category オブジェクトを返します。

In the examples above, two queries were made - one for the original object (e.g. a Category) and one for the related object(s) (e.g. the Product objects).

上記の例では、2 つのクエリが作成されました。1 つは元のオブジェクト (例: カテゴリ) 用で、もう 1 つは関連オブジェクト (例: Productobjects) 用です。

Tip

ヒント

Remember that you can see all of the queries made during a request via the web debug toolbar.

Web デバッグ ツールバーを介して、リクエスト中に行われたすべてのクエリを確認できることに注意してください。

If you know up front that you'll need to access both objects, you can avoid the second query by issuing a join in the original query. Add the following method to the ProductRepository class:

両方のオブジェクトにアクセスする必要があることが事前にわかっている場合は、元のクエリで結合を発行することにより、2 番目のクエリを回避できます。次のメソッドを ProductRepository クラスに追加します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Repository/ProductRepository.php

// ...
class ProductRepository extends ServiceEntityRepository
{
    public function findOneByIdJoinedToCategory(int $productId): ?Product
    {
        $entityManager = $this->getEntityManager();

        $query = $entityManager->createQuery(
            'SELECT p, c
            FROM App\Entity\Product p
            INNER JOIN p.category c
            WHERE p.id = :id'
        )->setParameter('id', $productId);

        return $query->getOneOrNullResult();
    }
}

This will still return an array of Product objects. But now, when you call $product->getCategory() and use that data, no second query is made.

これでも Product オブジェクトの配列が返されます。しかし、$product->getCategory() を呼び出してそのデータを使用すると、2 番目のクエリは作成されません。

Now, you can use this method in your controller to query for a Product object and its related Category in one query:

これで、コントローラーでこのメソッドを使用して、1 つのクエリで Productobject とそれに関連するカテゴリをクエリできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Controller/ProductController.php

// ...
class ProductController extends AbstractController
{
    public function show(ManagerRegistry $doctrine, int $id): Response
    {
        $product = $doctrine->getRepository(Product::class)->findOneByIdJoinedToCategory($id);

        $category = $product->getCategory();

        // ...
    }
}

Setting Information from the Inverse Side

So far, you've updated the relationship by calling $product->setCategory($category). This is no accident! Each relationship has two sides: in this example, Product.category is the owning side and Category.products is the inverse side.

ここまでで、$product->setCategory($category) を呼び出して関係を更新しました。これは偶然ではありません。各関係には 2 つの側面があります。この例では、Product.category が所有側であり、Category.products が反対側です。

To update a relationship in the database, you must set the relationship on the owning side. The owning side is always where the ManyToOne mapping is set (for a ManyToMany relation, you can choose which side is the owning side).

データベースの関係を更新するには、所有側で関係を設定する必要があります。所有側は常に、ManyToOne マッピングが設定されている場所です (ManyToMany リレーションの場合、どちらの側が所有側であるかを選択できます)。

Does this mean it's not possible to call $category->addProduct() or $category->removeProduct() to update the database? Actually, it is possible, thanks to some clever code that the make:entity command generated:

これは、データベースを更新するために $category->addProduct() または $category->removeProduct() を呼び出すことができないということですか?実際には、make:entity コマンドが生成した巧妙なコードのおかげで可能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Entity/Category.php

// ...
class Category
{
    // ...

    public function addProduct(Product $product): self
    {
        if (!$this->products->contains($product)) {
            $this->products[] = $product;
            $product->setCategory($this);
        }

        return $this;
    }
}

The key is $product->setCategory($this), which sets the owning side. Thanks, to this, when you save, the relationship will update in the database.

キーは、所有側を設定する $product->setCategory($this) です。これにより、保存すると、関係がデータベースで更新されます。

What about removing a Product from a Category? The make:entity command also generated a removeProduct() method:

カテゴリから製品を削除するのはどうですか? make:entity コマンドも removeProduct() メソッドを生成しました。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Entity/Category.php
namespace App\Entity;

// ...
class Category
{
    // ...

    public function removeProduct(Product $product): self
    {
        if ($this->products->contains($product)) {
            $this->products->removeElement($product);
            // set the owning side to null (unless already changed)
            if ($product->getCategory() === $this) {
                $product->setCategory(null);
            }
        }

        return $this;
    }
}

Thanks to this, if you call $category->removeProduct($product), the category_id on that Product will be set to null in the database.

これにより、$category->removeProduct($product) を呼び出すと、その Product がデータベースで null に設定されます。

But, instead of setting the category_id to null, what if you want the Product to be deleted if it becomes "orphaned" (i.e. without a Category)? To choose that behavior, use the orphanRemoval option inside Category:

しかし、category_id を null に設定する代わりに、製品が「孤立」した場合 (つまり、カテゴリがない場合) に製品を削除したい場合はどうすればよいでしょうか?その動作を選択するには、カテゴリ内で orphanRemoval オプションを使用します。
  • Attributes
    属性
1
2
3
4
5
6
// src/Entity/Category.php

// ...

#[ORM\OneToMany(targetEntity: Product::class, mappedBy: 'category', orphanRemoval: true)]
private $products;

Thanks to this, if the Product is removed from the Category, it will be removed from the database entirely.

これにより、製品がカテゴリから削除されると、データベースから完全に削除されます。

More Information on Associations

This section has been an introduction to one common type of entity relationship, the one-to-many relationship. For more advanced details and examples of how to use other types of relations (e.g. one-to-one, many-to-many), see Doctrine's Association Mapping Documentation.

このセクションでは、エンティティ関係の一般的なタイプの 1 つである 1 対多の関係について紹介しました。他のタイプのリレーション (1 対 1、多対多など) の使用方法のより高度な詳細と例については、Doctrine の Association Mapping Documentation を参照してください。

Note

ノート

If you're using attributes, you'll need to prepend all attributes with #[ORM\] (e.g. #[ORM\OneToMany]), which is not reflected in Doctrine's documentation.

属性を使用している場合、すべての属性の前に #[ORM\] (例: #[ORM\OneToMany]) を追加する必要がありますが、これは Doctrine のドキュメントには反映されていません。