我应该如何检查对象集合中的重复实体?

How should I check for dupplicate entity in an object collection?

我一直在读这个 post :

但我实际上不喜欢这个解决方案,因为学说已经提供了 contains() method, which have the advantage to keep logic directly into the object, and then to not load EXTRA_LAZY collections entirely

所以这里 Cart 实体拥有一个 CartProduct 集合:

/**
 * ...
 * @ORM\Entity(repositoryClass="App\Repository\CartRepository")
 */
abstract class Cart implements InheritanceInterface{
    ...
    /**
     * @ORM\OneToMany(targetEntity="CartProduct", mappedBy="cart", fetch="EXTRA_LAZY", cascade={"persist"})
     */
    private Collection $cartProducts;
    ...
    public function __construct()
    {
        $this->cartProducts = new ArrayCollection();
    }
    ...
}

(CartProduct 必须是一个实体,查看这个 simplify EA model. That's a standard way to proceed 相关实体持有额外字段)

现在我想添加一个新的 ProductCart 实体到我的 Cart class。

所以我要添加这个方法(由 Symfony make:entity 生成):

abstract class Cart implements InheritanceInterface{
...
    public function addCartProduct(CartProduct $cartProduct): self
    {
        if(!$this->getCartProducts()->contains($cartProduct)) {
            $this->cartProducts->add($cartProduct);
            $cartProduct->setCart($this);
        }
        return $this;
    }
...

然后我测试这段代码:

    public function testAddCartProduct()
    {
        $cart = new ShoppingCart($this->createMock(ShoppingCartState::class));
        $cart_product = new CartProduct();
        $cart_product->setProduct(new Product(self::NO_.'1', new Group('1')));
        $cart->addCartProduct($cart_product);
        $cart_product2 = new CartProduct();
        $cart_product2->setProduct(new Product(self::NO_.'1', new Group('1')));
        $cart->addCartProduct($cart_product2);
        $this->assertCount(1, $cart->getCartProducts());
    }

但是当我运行这个测试时,它失败了:

Failed asserting that actual size 2 matches expected size 1.

所以我检查了一下,Cart.cartProducts Collection 有两个完全相同的对象的产品。

因为它是一个 ArrayCollection,我想它只是使用这个方法:

namespace Doctrine\Common\Collections;
class ArrayCollection implements Collection, Selectable {
    ...
    public function contains($element)
    {
        return in_array($element, $this->elements, true);
    }

那么好吧,当然在这种情况下它只是return false,并且对象被认为是不同的。

所以现在,我希望在实现 Collection 对象时可以使用 PersistentCollection 而不是 ArrayCollection,因为 PersistentCollection.contains() 方法看起来更好。

abstract class Cart implements InheritanceInterface{
...
 public function __construct()
    {
--      $this->cartProducts = new ArrayCollection();
++      $this->cartProducts = new PersistentCollection(...);
    }
}

但这需要一个 EntityManager 作为参数,因此,将 EntityManager 赋予 Entity 对象有点矫枉过正...

所以我最后不知道什么是检查集合中重复实体的更好方法。

当然,我可以自己实现这样的东西:

abstract class Cart implements InheritanceInterface{
...
    public function addCartProduct(CartProduct $cartProduct): self
    {
        if(!$this->getCartProducts()->filter(
            function (CartProduct $cp)use($cartProduct){
                return $cp->getId() === $cartProduct->getId();
            })->count()) {
            $this->cartProducts->add($cartProduct);
            $cartProduct->setCart($this);
        }
        return $this;
    }
...

但它需要加载每个实体,我真的不喜欢这个主意。

我个人同意你的意见,我认为实体本身不应该有责任确保没有重复。

该实体无法像存储库那样发出请求,而且我不明白您如何在不查询的情况下确定数据库中没有重复项。

调用 contains 不会在您的情况下触发提取,这意味着集合将保持原样,无论如何这不是您想要的,因为您可能有一个以前持久化的副本,但不会成为其中的一部分集合,因为您将其标记为 EXTRA_LAZY.

您也不希望获取集合中的所有实体(并将结果转换为对象)只是为了检查是否发生碰撞。

所以恕我直言,您应该在实体的存储库中创建一个方法来检查重复项,一个简单的 SELECT COUNT(id)

那就是你真正的问题了。

你做测试的方式永远不会发现碰撞。当你这样做时:

$cart = new ShoppingCart($this->createMock(ShoppingCartState::class));
$cart_product = new CartProduct();
$cart_product->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product);
$cart_product2 = new CartProduct();
$cart_product2->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product2);
$this->assertCount(1, $cart->getCartProducts());

您正在创建 CartProduct 两个 个实例,这就是为什么调用 contains 找不到任何东西。

因为 contains 检查对象引用,而不是内容,就像您在其实现中看到的那样:

public function contains($element)
{
    return in_array($element, $this->elements, true);
}

因此在您的测试用例中,您真正测试的是:

in_array(new CartProduct(), [new CartProduct()], true);

这将始终 return 错误。