1

I have an Account entity which contains a collection of Group entities. In addition, each Account must have a default Group. How can one persist the entities? When attempting to do so, $manager->flush(); complains with a Not null violation for the entity that was persisted first.

Account:

namespace App\Entity; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Symfony\Bridge\Doctrine\IdGenerator\UuidV4Generator; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() */ class Account { /** * @ORM\Id * @ORM\Column(type="uuid", unique=true) * @ORM\GeneratedValue(strategy="CUSTOM") * @ORM\CustomIdGenerator(class=UuidV4Generator::class) */ private $id; /** * @ORM\OneToOne(targetEntity=Group::class, cascade={"persist", "remove"}) * @ORM\JoinColumn(nullable=false, unique=true) */ private $defaultGroup; /** * @ORM\OneToMany(targetEntity=Group::class, mappedBy="account") */ private $groups; public function __construct() { $this->groups = new ArrayCollection(); $this->defaultGroup = new Group(); } // Typical getters and setters } 

Group:

namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() */ class Group { /** * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ private $id; /** * @ORM\ManyToOne(targetEntity=Account::class, inversedBy="groups") * @ORM\JoinColumn(nullable=false, onDelete="CASCADE") */ private $account; // Typical getters and setters } 
2
  • Where are you setting this default group? Is it already persisted? post this part of the code as well pls Commented Feb 22, 2021 at 21:02
  • @FelipeChagas No, not already persisted because it must first contain an Account. But I can't persist the account first because the Account must contain a Group. Looks like this can be solved DP specific with Postgresql's deferred constraints, but maybe best not to attempt to do so at the DB level. The only code is where I set the default group in Account's constructor as shown in the original post. Thanks Commented Feb 22, 2021 at 22:24

1 Answer 1

2

Your approach is not possible because Group::$account is already mapped to the one-to-many Account::$groups property. Doctrine does not know what to do when you persist a Group via Account::$defaultGroup, as this is not the property Group::$account is inversing.

You will have to enforce this Account default group invariant yourself programmatically. There's multiple approaches to this, but I suggest using the existing Account::$groups mapping in combination with the Symfony Collection validation constraint. Validating that an Account should have at least 1 Group assigned to it, which will be the default one.

Here's an example:

Because the entity name Group was giving me syntax problems in the SQL dialect I was using I renamed it to Team.

The Account implementation:

<?php declare(strict_types = 1); namespace App\Entity; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; /** * @ORM\Entity */ class Account { /** * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ private ?int $id = null; /** * @ORM\OneToMany( * targetEntity="App\Entity\Team", * mappedBy="account", * cascade={"persist", "remove"}, * ) * @Assert\Count( * min=1 * ) */ private Collection $teams; public function __construct() { $this->teams = new ArrayCollection(); $this->addTeam(new Team()); } public function getId(): ?int { return $this->id; } public function getDefaultTeam(): ?Team { $team = $this->teams->first(); return $team instanceof Team ? $team : null; } public function getTeams(): Collection { return $this->teams; } public function setTeams(array $teams): void { $this->teams->clear(); foreach ($teams as $team) { if ($team instanceof Team) { $this->addTeam($team); } } } public function addTeam(Team $team): void { if (false === $this->teams->contains($team)) { $this->teams->add($team); $team->setAccount($this); } } public function removeTeam(Team $team): void { if (true === $this->teams->contains($team)) { $this->teams->removeElement($team); } } } 

The Team implementation:

<?php declare(strict_types = 1); namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Team { /** * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ private ?int $id = null; /** * @ORM\ManyToOne( * targetEntity="App\Entity\Account", * inversedBy="teams", * ) * @ORM\JoinColumn( * name="account_id", * referencedColumnName="id", * nullable=false * ) */ private ?Account $account = null; public function getId(): ?int { return $this->id; } public function getAccount(): ?Account { return $this->account; } public function setAccount(Account $account): void { $this->account = $account; } } 
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you Jeroen, I am very new to Symfony and always in the past tried to enforce constraints at the DB level. I've spent more time investigating and believe it can be done using Postgresql deferable constraints, but maybe not worth it and just doing it with the app.
I understand completely. I was also curious if this can be enforced by the database (or more specifically by the ORM), but unfortunately concluded that it can not. At least not in a simple well documented way. Good luck with whatever approach you choose to go with!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.