doctrine一对一关系需明确主/从实体:外键通常在从属实体(如profile含user_id),主实体用mappedby;少数情况外键在主实体(如user含profile_id),此时主实体用joincolumn、从属实体用mappedby;可选与必需由nullable和php类型共同决定。

Doctrine 中的一对一关系,核心在于明确哪边是“主实体”(principal),哪边是“从属实体”(dependent)——这直接决定外键放在哪张表、导航属性怎么写、级联行为如何生效。不是简单加个 @OneToOne 就完事,必须配合 @JoinColumn 或 mappedBy 指明物理约束和逻辑归属。
外键在从属实体上(最常见)
这是数据库设计中最自然的方式:从属表带外键,指向主表主键。比如 User 和 Profile,profile 表里有 user_id 字段。
-
主实体(User):声明
@OneToOne+mappedBy,不持有外键,只提供反向导航 -
从属实体(Profile):声明
@OneToOne+@JoinColumn,显式指定外键字段名和引用目标
示例:
#[ORM\Entity]
class User
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\OneToOne(mappedBy: 'user', targetEntity: Profile::class, cascade: ['persist', 'remove'])]
private ?Profile $profile = null;
}
#[ORM\Entity]
class Profile
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\OneToOne(inversedBy: 'user', targetEntity: User::class)]
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')]
private ?User $user = null;
}
外键在主实体上(较少见)
当主表自己存了从属记录的 ID(例如 users.profile_id),就属于这种模式。Doctrine 要求主实体用 @JoinColumn,从属实体用 mappedBy。
-
主实体(User):含外键字段,用
@JoinColumn关联到Profile.id -
从属实体(Profile):无外键字段,仅通过
mappedBy声明反向关系
注意:此时 Profile 的 $user 导航属性必须可空,因为外键在对方表里。
可选与必需的区别
是否允许为 null,由外键字段的数据库约束和 PHP 属性类型共同决定。
- 若
@JoinColumn(nullable: false)且 PHP 属性类型为?Type或Type|null,表示“可选一对一”(存在或不存在) - 若外键字段设为
NOT NULL且 PHP 属性类型为Type(非空),Doctrine 会要求每次 persistUser时必须同时 persistProfile,即“必需一对一”
实际开发中,绝大多数场景用的是“可选一对一”,比如用户不一定有完整资料。
API Platform 中的序列化控制
默认情况下,API Platform 会把关联实体整个嵌套输出。如需只返回 ID 或禁用嵌套,可在属性上加:
-
#[ApiProperty(readableLink: false, writableLink: false)]:禁用 IRI 链接,只输出 ID -
#[Groups(['user:read'])]:配合序列化组精细控制输出字段
避免 N+1 查询,记得在 Repository 或 DQL 中用 join 预加载:$qb->leftJoin('u.profile', 'p')。











