如何用@UniqueEntity实现同一Perimeter下SiretNumber的唯一约束?
实现同一Perimeter下Supplier的siretNumber唯一校验
我需要实现的需求是:仅当同一Perimeter下已存在关联该siretNumber的Supplier时,才校验siretNumber的唯一性。也就是说不同Perimeter可以拥有相同siretNumber的Supplier,但同一Perimeter下的Supplier的siretNumber必须是唯一的。
现有Supplier实体代码
<?php /** * @ORM\Entity(repositoryClass=SupplierRepository::class) * @UniqueEntity("siretNumber",repositoryMethod="getSupplierByPerimeter", message="Le numéro de SIRET est déjà utilisé.") */ class Supplier { /** * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ private $id; /** * @Assert\NotBlank(allowNull=true) * @Assert\Regex(pattern = "/^[0-9]*$", message="Seul les chiffres sont autorisés.") * @Assert\Length( * min=14, * max=14, * exactMessage = "Le numéro de SIRET doit faire exactement 14 chiffres (SIREN (9) et NIC (5))" * ) * @ORM\Column(type="string", length=14, nullable=true, unique=true) */ private $siretNumber; /** * @ORM\ManyToOne(targetEntity=Perimeter::class, inversedBy="suppliers") * @ORM\JoinColumn(nullable=false) */ private $perimeter; }
尝试的仓库方法实现
我尝试使用@UniqueEntity的repositoryMethod参数,对应的仓库方法代码如下:
<?php namespace App\Repository; use App\Entity\Supplier; use App\Repository\PerimeterRepository; use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\Security\Core\Security; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; /** * @method Supplier|null find($id, $lockMode = null, $lockVersion = null) * @method Supplier|null findOneBy(array $criteria, array $orderBy = null) * @method Supplier[] findAll() * @method Supplier[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class SupplierRepository extends ServiceEntityRepository { private $security; private $perimeterRepository; public function __construct(ManagerRegistry $registry, Security $security,PerimeterRepository $perimeterRepository) { parent::__construct($registry, Supplier::class); $this->security = $security; $this->perimeterRepository = $perimeterRepository; } /** * @return Supplier[] Returns an array of Supplier objects */ public function getSupplierByPerimeter() { $perimeter = $this->perimeterRepository->findOneBy(["label" => $this->security->getUser()->getOptions()["perimeter"]]); return $this->createQueryBuilder('s') ->where('s.perimeter = :perimeter') ->setParameter('perimeter', $perimeter) ->getQuery() ->getResult() ; } }
遇到的问题
实际运行时,@UniqueEntity仍在整个Supplier表中校验唯一性,而非仅校验getSupplierByPerimeter返回的结果集。因此即使siretNumber属于其他Perimeter,创建新Supplier时仍会触发校验错误。我可能未正确使用repositoryMethod,请问该如何实现需求?
问题根源与解决方案
核心问题分析
- 数据库层面全局唯一限制:实体类中
siretNumber字段的@ORM\Column(unique=true)属性,会让Doctrine在数据库创建全局唯一索引,直接导致跨Perimeter的siretNumber无法重复,这是最根本的问题。 - repositoryMethod用法错误:该方法需要接收待校验的实体对象作为参数,根据实体自身的
perimeter和siretNumber查询重复,而非返回当前用户所属Perimeter的所有Supplier。
正确实现步骤
方式一:使用@UniqueEntity联合字段校验(简洁方案)
直接通过@UniqueEntity指定联合字段,实现同一Perimeter下的siretNumber唯一:
/** * @ORM\Entity(repositoryClass=SupplierRepository::class) * @UniqueEntity( * fields={"siretNumber", "perimeter"}, * message="Ce numéro de SIRET existe déjà dans ce périmètre." * ) * @ORM\Table(uniqueConstraints={ * @ORM\UniqueConstraint(name="idx_supplier_siret_perimeter", columns={"siret_number", "perimeter_id"}) * }) */ class Supplier { // ...其他字段保持不变 /** * @Assert\NotBlank(allowNull=true) * @Assert\Regex(pattern = "/^[0-9]*$", message="Seul les chiffres sont autorisés.") * @Assert\Length( * min=14, * max=14, * exactMessage = "Le numéro de SIRET doit faire exactement 14 chiffres (SIREN (9) et NIC (5))" * ) * @ORM\Column(type="string", length=14, nullable=true) // 移除unique=true */ private $siretNumber; // ...perimeter字段保持不变 }
- 移除
siretNumber的unique=true属性,避免全局唯一限制 - 通过
fields={"siretNumber", "perimeter"}指定联合校验规则 - 推荐添加数据库层面的联合唯一索引(通过
@ORM\Table注解),避免并发场景下的数据重复
方式二:正确使用repositoryMethod(复杂场景方案)
如果需要更复杂的校验逻辑,可自定义仓库方法:
- 修改实体注解:
/** * @ORM\Entity(repositoryClass=SupplierRepository::class) * @UniqueEntity( * fields="siretNumber", * repositoryMethod="findExistingSupplierByPerimeterAndSiret", * message="Ce numéro de SIRET existe déjà dans ce périmètre." * ) */ class Supplier { // ...移除siretNumber的unique=true,其他字段不变 }
- 修改仓库方法,接收待校验实体并查询重复:
/** * 检查同一Perimeter下是否存在相同siretNumber的Supplier * @param Supplier $supplier * @return Supplier|null */ public function findExistingSupplierByPerimeterAndSiret(Supplier $supplier): ?Supplier { $qb = $this->createQueryBuilder('s') ->where('s.siretNumber = :siret') ->andWhere('s.perimeter = :perimeter') ->setParameter('siret', $supplier->getSiretNumber()) ->setParameter('perimeter', $supplier->getPerimeter()); // 编辑场景下排除当前实体自身 if ($supplier->getId()) { $qb->andWhere('s.id != :id') ->setParameter('id', $supplier->getId()); } return $qb->getQuery()->getOneOrNullResult(); }
内容的提问来源于stack exchange,提问作者Laura Leroy
相关产品推荐
相关产品推荐

