关联关系 #

一、关联关系概述 #

1.1 关联类型 #

text
┌─────────────────────────────────────────────────────┐
│                  Doctrine 关联类型                  │
├─────────────────────────────────────────────────────┤
│                                                     │
│  一对一 (OneToOne)                                  │
│  ├── 用户 - 资料                                    │
│  └── 订单 - 支付信息                                │
│                                                     │
│  一对多 (OneToMany) / 多对一 (ManyToOne)            │
│  ├── 用户 - 文章                                    │
│  └── 分类 - 产品                                    │
│                                                     │
│  多对多 (ManyToMany)                                │
│  ├── 学生 - 课程                                    │
│  └── 文章 - 标签                                    │
│                                                     │
└─────────────────────────────────────────────────────┘

1.2 关联选项 #

选项 说明
cascade 级联操作
fetch 加载策略(LAZY/EAGER)
orphanRemoval 孤儿删除
inversedBy 反向关联
mappedBy 映射关联

二、一对一关系 #

2.1 单向一对一 #

php
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class User
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\OneToOne]
    private ?Profile $profile = null;
}

#[ORM\Entity]
class Profile
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $bio = null;
}

2.2 双向一对一 #

php
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class User
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\OneToOne(inversedBy: 'user', cascade: ['persist', 'remove'])]
    #[ORM\JoinColumn(nullable: false)]
    private ?Profile $profile = null;

    public function getProfile(): ?Profile
    {
        return $this->profile;
    }

    public function setProfile(Profile $profile): static
    {
        $this->profile = $profile;
        return $this;
    }
}

#[ORM\Entity]
class Profile
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $bio = null;

    #[ORM\OneToOne(mappedBy: 'user')]
    private ?User $user = null;

    public function getUser(): ?User
    {
        return $this->user;
    }

    public function setUser(?User $user): static
    {
        $this->user = $user;
        return $this;
    }
}

三、一对多关系 #

3.1 基本一对多 #

php
<?php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Category
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 100)]
    private ?string $name = null;

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

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

    public function getProducts(): Collection
    {
        return $this->products;
    }

    public function addProduct(Product $product): static
    {
        if (!$this->products->contains($product)) {
            $this->products->add($product);
            $product->setCategory($this);
        }

        return $this;
    }

    public function removeProduct(Product $product): static
    {
        if ($this->products->removeElement($product)) {
            if ($product->getCategory() === $this) {
                $product->setCategory(null);
            }
        }

        return $this;
    }
}

#[ORM\Entity]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $name = null;

    #[ORM\ManyToOne(inversedBy: 'products')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Category $category = null;

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

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

3.2 级联操作 #

php
<?php

#[ORM\Entity]
class Author
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\OneToMany(
        mappedBy: 'author',
        targetEntity: Article::class,
        cascade: ['persist', 'remove'],
        orphanRemoval: true
    )]
    private Collection $articles;

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

#[ORM\Entity]
class Article
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\ManyToOne(inversedBy: 'articles')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Author $author = null;
}

3.3 排序关联 #

php
<?php

#[ORM\Entity]
class Category
{
    #[ORM\OneToMany(
        mappedBy: 'category',
        targetEntity: Product::class,
        orderBy: ['createdAt' => 'DESC', 'name' => 'ASC']
    )]
    private Collection $products;
}

四、多对多关系 #

4.1 基本多对多 #

php
<?php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Student
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 100)]
    private ?string $name = null;

    #[ORM\ManyToMany(targetEntity: Course::class, inversedBy: 'students')]
    private Collection $courses;

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

    public function getCourses(): Collection
    {
        return $this->courses;
    }

    public function addCourse(Course $course): static
    {
        if (!$this->courses->contains($course)) {
            $this->courses->add($course);
            $course->addStudent($this);
        }

        return $this;
    }

    public function removeCourse(Course $course): static
    {
        if ($this->courses->removeElement($course)) {
            $course->removeStudent($this);
        }

        return $this;
    }
}

#[ORM\Entity]
class Course
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 100)]
    private ?string $name = null;

    #[ORM\ManyToMany(targetEntity: Student::class, mappedBy: 'courses')]
    private Collection $students;

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

    public function getStudents(): Collection
    {
        return $this->students;
    }

    public function addStudent(Student $student): static
    {
        if (!$this->students->contains($student)) {
            $this->students->add($student);
        }

        return $this;
    }

    public function removeStudent(Student $student): static
    {
        $this->students->removeElement($student);
        return $this;
    }
}

4.2 自定义中间表 #

php
<?php

#[ORM\Entity]
class Article
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'articles')]
    #[ORM\JoinTable(name: 'article_tags')]
    #[ORM\JoinColumn(name: 'article_id', referencedColumnName: 'id')]
    #[ORM\InverseJoinColumn(name: 'tag_id', referencedColumnName: 'id')]
    private Collection $tags;
}

#[ORM\Entity]
class Tag
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 50)]
    private ?string $name = null;

    #[ORM\ManyToMany(targetEntity: Article::class, mappedBy: 'tags')]
    private Collection $articles;
}

五、关联中间实体 #

5.1 带属性的多对多 #

php
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class User
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserGroup::class)]
    private Collection $userGroups;
}

#[ORM\Entity]
class Group
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 100)]
    private ?string $name = null;

    #[ORM\OneToMany(mappedBy: 'group', targetEntity: UserGroup::class)]
    private Collection $userGroups;
}

#[ORM\Entity]
class UserGroup
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column]
    private ?\DateTimeImmutable $joinedAt = null;

    #[ORM\Column(length: 20)]
    private ?string $role = null;

    #[ORM\ManyToOne(inversedBy: 'userGroups')]
    #[ORM\JoinColumn(nullable: false)]
    private ?User $user = null;

    #[ORM\ManyToOne(inversedBy: 'userGroups')]
    #[ORM\JoinColumn(nullable: false)]
    private ?Group $group = null;
}

六、自引用关联 #

6.1 自引用一对多 #

php
<?php

#[ORM\Entity]
class Category
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 100)]
    private ?string $name = null;

    #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
    private ?self $parent = null;

    #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
    private Collection $children;

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

6.2 自引用多对多 #

php
<?php

#[ORM\Entity]
class User
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 100)]
    private ?string $name = null;

    #[ORM\ManyToMany(targetEntity: self::class)]
    #[ORM\JoinTable(name: 'user_friends')]
    #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')]
    #[ORM\InverseJoinColumn(name: 'friend_id', referencedColumnName: 'id')]
    private Collection $friends;

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

七、加载策略 #

7.1 延迟加载(默认) #

php
<?php

#[ORM\ManyToOne]
#[ORM\JoinColumn(name: 'category_id')]
private ?Category $category = null;

7.2 预加载 #

php
<?php

#[ORM\ManyToOne(fetch: 'EAGER')]
#[ORM\JoinColumn(name: 'category_id')]
private ?Category $category = null;

7.3 额外加载 #

php
<?php

#[ORM\ManyToOne(fetch: 'EXTRA_LAZY')]
#[ORM\JoinColumn(name: 'category_id')]
private ?Category $category = null;

八、关联查询 #

8.1 关联查询示例 #

php
<?php

class ArticleRepository extends ServiceEntityRepository
{
    public function findWithAuthor(): array
    {
        return $this->createQueryBuilder('a')
            ->innerJoin('a.author', 'u')
            ->addSelect('u')
            ->getQuery()
            ->getResult();
    }

    public function findWithTags(): array
    {
        return $this->createQueryBuilder('a')
            ->leftJoin('a.tags', 't')
            ->addSelect('t')
            ->getQuery()
            ->getResult();
    }

    public function findByTagName(string $tagName): array
    {
        return $this->createQueryBuilder('a')
            ->innerJoin('a.tags', 't')
            ->where('t.name = :name')
            ->setParameter('name', $tagName)
            ->getQuery()
            ->getResult();
    }
}

九、最佳实践 #

9.1 关联设计原则 #

text
关联设计原则:
├── 选择合适的关联类型
├── 合理使用级联操作
├── 注意循环引用问题
├── 使用双向关联时维护双向一致性
├── 避免过度使用EAGER加载
└── 大数据量关联使用EXTRA_LAZY

9.2 双向关联维护 #

php
<?php

class User
{
    public function addArticle(Article $article): static
    {
        if (!$this->articles->contains($article)) {
            $this->articles->add($article);
            $article->setAuthor($this);
        }

        return $this;
    }

    public function removeArticle(Article $article): static
    {
        if ($this->articles->removeElement($article)) {
            if ($article->getAuthor() === $this) {
                $article->setAuthor(null);
            }
        }

        return $this;
    }
}

十、总结 #

本章学习了:

  • 关联关系类型
  • 一对一关系配置
  • 一对多/多对一关系
  • 多对多关系
  • 关联中间实体
  • 自引用关联
  • 加载策略
  • 关联查询

下一章将学习 表单基础

最后更新:2026-03-28