UnitOfWork state formation when loading entity with inverse side #6485

Open
opened 2026-01-22 15:33:56 +01:00 by admin · 0 comments
Owner

Originally created by @berrymore on GitHub (Jun 10, 2020).

I just want to understand is it expected behavior.

I have two entities.

<?php

namespace Issue\Entity\Budget;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="budgets")
 */
class Budget
{

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     * @var int|null
     */
    protected $id;

    /**
     * @ORM\Column(type="integer")
     * @var int
     */
    protected $amount;

    /**
     * @ORM\OneToOne(targetEntity="BudgetSpendingLimit", mappedBy="budget", orphanRemoval=true, cascade={"all"})
     * @var \Issue\Entity\Budget\BudgetSpendingLimit|null
     */
    protected $spendingLimit;

    public function __construct()
    {
        $this->id            = null;
        $this->amount        = 0;
        $this->spendingLimit = null;
    }

    public function applySpendingLimit(int $limit): void
    {
        $this->spendingLimit = new BudgetSpendingLimit($this, $limit);
    }

    public function offSpendingLimit(): void
    {
        $this->spendingLimit = null;
    }
}
<?php

namespace Issue\Entity\Budget;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="budget_spending_limits")
 */
class BudgetSpendingLimit
{

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     * @var int|null
     */
    protected $id;

    /**
     * @ORM\OneToOne(targetEntity="Budget", inversedBy="spendingLimit")
     * @ORM\JoinColumn(name="budget_id")
     * @var \Issue\Entity\Budget\Budget
     */
    protected $budget;

    /**
     * @ORM\Column(type="integer")
     * @var int
     */
    protected $hiLimit;

    public function __construct(Budget $budget, int $hiLimit)
    {
        $this->id      = null;
        $this->budget  = $budget;
        $this->hiLimit = $hiLimit;
    }
}

Budget is an aggregate and my goal is to control limits through the Budget entity. When I call offSpendingLimit() I expect that row in a budget_spending_limits would be deleted.

The next snippet is working as expected.

$budget = $em->createQueryBuilder()
    ->select('b, l')
    ->from(Budget::class, 'b')
    ->leftJoin('b.spendingLimit', 'l')
    ->where('b = :id')
    ->setParameter('id', 9)
    ->getQuery()
    ->getSingleResult();

$budget->offSpendingLimit();

$em->persist($budget);
$em->flush();

But if I leave "leftJoin" then the code just does nothing though "BudgetSpendingLimit" would be force fetched by the second query produced by Doctrine.

$budget = $em->createQueryBuilder()
    ->select('b')
    ->from(Budget::class, 'b')
    ->where('b = :id')
    ->setParameter('id', 9)
    ->getQuery()
    ->getSingleResult();

$budget->offSpendingLimit();

$em->persist($budget);
$em->flush();

As I understood the main difference between those two snippets is how Doctrine fills out the UnitOfWork "originalEntityData" state.

In the first case "originalEntityData" for "Budget" has "spendingLimit" property with the appropriate entity. So when I do flush Doctrine computes changes correctly.

In the second case "originalEntityData" for "Budget" doesn't have the "spendingLimit" property at all.

Is it expected Doctrine behavior? Is there an "easy" way to make code as in the second snippet (without join) work or I need to use join explicitly?

Originally created by @berrymore on GitHub (Jun 10, 2020). I just want to understand is it expected behavior. I have two entities. ```php <?php namespace Issue\Entity\Budget; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="budgets") */ class Budget { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue * @var int|null */ protected $id; /** * @ORM\Column(type="integer") * @var int */ protected $amount; /** * @ORM\OneToOne(targetEntity="BudgetSpendingLimit", mappedBy="budget", orphanRemoval=true, cascade={"all"}) * @var \Issue\Entity\Budget\BudgetSpendingLimit|null */ protected $spendingLimit; public function __construct() { $this->id = null; $this->amount = 0; $this->spendingLimit = null; } public function applySpendingLimit(int $limit): void { $this->spendingLimit = new BudgetSpendingLimit($this, $limit); } public function offSpendingLimit(): void { $this->spendingLimit = null; } } ``` ```php <?php namespace Issue\Entity\Budget; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="budget_spending_limits") */ class BudgetSpendingLimit { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue * @var int|null */ protected $id; /** * @ORM\OneToOne(targetEntity="Budget", inversedBy="spendingLimit") * @ORM\JoinColumn(name="budget_id") * @var \Issue\Entity\Budget\Budget */ protected $budget; /** * @ORM\Column(type="integer") * @var int */ protected $hiLimit; public function __construct(Budget $budget, int $hiLimit) { $this->id = null; $this->budget = $budget; $this->hiLimit = $hiLimit; } } ``` Budget is an aggregate and my goal is to control limits through the Budget entity. When I call `offSpendingLimit()` I expect that row in a `budget_spending_limits` would be deleted. The next snippet is working as expected. ```php $budget = $em->createQueryBuilder() ->select('b, l') ->from(Budget::class, 'b') ->leftJoin('b.spendingLimit', 'l') ->where('b = :id') ->setParameter('id', 9) ->getQuery() ->getSingleResult(); $budget->offSpendingLimit(); $em->persist($budget); $em->flush(); ``` But if I leave "leftJoin" then the code just does nothing though "BudgetSpendingLimit" would be force fetched by the second query produced by Doctrine. ```php $budget = $em->createQueryBuilder() ->select('b') ->from(Budget::class, 'b') ->where('b = :id') ->setParameter('id', 9) ->getQuery() ->getSingleResult(); $budget->offSpendingLimit(); $em->persist($budget); $em->flush(); ``` As I understood the main difference between those two snippets is how Doctrine fills out the UnitOfWork "originalEntityData" state. In the first case "originalEntityData" for "Budget" has "spendingLimit" property with the appropriate entity. So when I do flush Doctrine computes changes correctly. In the second case "originalEntityData" for "Budget" doesn't have the "spendingLimit" property at all. Is it expected Doctrine behavior? Is there an "easy" way to make code as in the second snippet (without join) work or I need to use join explicitly?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#6485