关联关系 #
一、关联关系概述 #
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