Doctrine inserts twice when adding one related entity and using $em->clear() - symfony

When I try to add new info item it either inserts two of them or none. In the state of code below it inserts two. If i comment something it inserts none. When I comment the line with $em->clear() it inserts exactly one, as I need it. What I don't understand and what I'm doing wrong?
$limit = 10;
$offset = 0;
do {
$products = $selectedModelQuery->getQuery()
->setFirstResult($limit * $offset)
->setMaxResults($limit)->getResult();
$offset++;
$count = count($products);
/** #var \Nti\ApiBundle\Entity\Product $product */
foreach ($selectedModelQuery->getQuery()->getResult() as $product) {
if (!$product->getCollections()->contains($productCollection)) {
$product->addCollection($productCollection);
$productInfo = new ProductInfo;
$productInfo->setProduct($product);
$productInfo->setData($productCollection->getMessage());
$productInfo->setInfoType(ProductInfoInterface::TYPE_CUSTOM);
//$em->merge($product);
//$em->persist($productInfo);
}
}
$em->flush();
$em->clear();
} while ($count);
Main Entity:
class Product {
/**
* #ORM\OneToMany(targetEntity="ProductInfo", mappedBy="product", cascade={"persist", "remove"})
* #var ProductInfoInterface[]|ArrayCollection
*/
protected $infoList;
....
}
Related Entity:
class ProductInfo {
/**
* #ORM\ManyToOne(targetEntity="\Nti\ApiBundle\Entity\Product", inversedBy="infoList")
* #ORM\JoinColumn(name="product_id", referencedColumnName="id", onDelete="CASCADE", nullable=false)
*/
protected $product;
...
}

Doctrine is based on Identity Map that keeps reference to all handled object (ie.: if you try to query two times for the same record by id, first time you hit the db but the second not and object is loaded from Identity Map).
When you use $em->clear() you are "discarding" all objects into Identity Map so, next iteration of do ... while will result in a brand new object (from ORM point of view) that will be flagged for writing operation.

Related

Looping through entities and updating them causes error on flush

I am new to symfony and doctrine. And I am compeleting a code that someone else has started. I mainly have a form for which I wrote a validation function in my controller. In this form a BusReservation object along with its BusReservationDetails are created and saved to the db. so at the end of the form validation function, after the entities are saved in DB, I call a BusReservation Manager method which is transformBusReservationDetailIntoBusTicket which aim is to take each BusReservationDetail in the BusReservation oject and create a a new entity BusTicket based on it.
so I created this loop (please let me know if there is something wrong in my code so that i can write in a good syntax). I tried to put the 3 persist that you see at the end of the code but I got : Notice: Undefined index: 0000000..
I tried to merge (the last 3 lines in code ) I got the following :
A new entity was found through the relationship 'MyBundle\Entity\CustomInfo#busTicket' that was not configured to cascade persist operations for entity: . To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}).
I got this same error when i commented all theh 6 lines of merge and flush.
PS: I am not expecting the flush to fully work. There are some properties that are nullable=false so I assume that I must set them as well so that the entities can be saved to DB. But the error i got is by far different than this.
PS : I noticed that there is a onFlush where the customInfo is updated and persisted again and other things happen, but i am trying to debug step by step. I tried to detach this event but still got the same errors. so I want to fix my code and make sure that the code part that i wrote in the manager is correct and if that's the case then I can move to debugging the event Listener. so please I would like to know if the following code is correct and why the flush is not working.
/**
* #param $idBusReservation
* #return bool
* #throws \Doctrine\ORM\NonUniqueResultException
*/
public function transformBusReservationIntoBusTicket($idBusReservation): bool
{ $result = "into the function";
/** #var BusReservation $busReservation */
$busReservation = $this->em->getRepository('MyBundle:BusReservation')->find($idBusReservation);
if ($busReservation !== null) {
/** #var BusReservationDetail $busReservationDetail */
foreach ($busReservation->getBusReservationDetails() as $busReservationDetail) {
$busTicket = new BusTicket($busReservationDetail->getBusModel(), $busReservation->getPassenger());
$busReservationDetail->setBusTicket($busTicket);
$busTicket->setBusReservationDetail($busReservationDetail);
$busTicket->setOwner($busreservation->getPassenger()->getName());
if ($busReservationDetail->getBusModel()->getCode() === 'VIPbus') {
// perform some logic .. later on
} else {
$customInfo = new CustomInfo();
$customInfo->setNumber(1551998);
// $customInfo->setCurrentMode(
// $this->em->getRepository('MyBundle:Mode')
// ->find(['code' => 'Working'])
// );
$customInfo->setBusTicket($busTicket);
// Bus ticket :
$busTicket->addCustomInfo($customInfo);
$busTicket->setComment($busReservation->getComment());
}
/** #var Mode $currentMode */
$currentMode = $this->em->getRepository('MyBundle:Mode')
->findOneBy(['code' => 'Working']);
$busTicket->setCurrentMode($currentMode);
// $this->em->merge($customInfo);
// $this->em->merge($busReservationDetail);
// $this->em->merge($busTicket);
// $this->em->persist($customInfo);
// $this->em->persist($busReservationDetail);
// $this->em->persist($busTicket);
}
$this->em->flush();
// $this->em->clear();
}
return $result;
}
// *************** In BusReservation.php ********************
/**
* #ORM\OneToMany(targetEntity="MyBundle\Entity\BusReservationDetail", mappedBy="busReservation")
*/
private $busReservationDetails;
/**
* Get busReservationDetails
*
*#return Collection
*/
public function getBusReservationDetails()
{
return $this->busReservationDetails;
}
// ---------------------------------------------------------------------
// *************** In BusReservationDetail.php ********************
/**
* #ORM\ManyToOne(targetEntity="MyBundle\Entity\BusReservation", inversedBy="busReservationDetails")
* #ORM\JoinColumn(name="id_bus_reservation", referencedColumnName="id_bus_reservation", nullable=false)
*/
private $busReservation;
/**
* #ORM\ManyToOne(targetEntity="MyBundle\Entity\BusModel")
* #ORM\JoinColumn(name="bus_model_code", referencedColumnName="bus_model_code", nullable=false)
*/
private $busModel;
/**
* #ORM\OneToOne(targetEntity="MyBundle\Entity\BusTicket", inversedBy="busReservationDetail", cascade={"merge","remove","persist"})
* #ORM\JoinColumn(name="id_bus_ticket", referencedColumnName="id_bus_ticket")
*/
private $busTicket;
/**
* #return BusModel
*/
public function getBusModel()
{
return $this->busModel;
}
//-------------------------------------------------------------------------
// ************ IN BusTicket.php *****************************
/**
* #ORM\OneToMany(targetEntity="MyBundle\Entity\CustomInfo", mappedBy="busTicket")
*/
private $customInfos;
/**
*
* #param CustomInfo $customInfo
*
* #return BusTicket
*/
public function addCustomInfot(CustomInfo $customInfo)
{
if (!$this->customInfos->contains($customInfo)) {
$this->customInfos[] = $customInfo;
}
return $this;
}
/**
* #ORM\OneToOne(targetEntity="MyBundle\Entity\busReservationDetail", mappedBy="busTicket")
*/
private $busReservationDetail;
// --------------------------------------------------------------------
// CUSTOMINFO ENTITY
/**
* #ORM\ManyToOne(targetEntity="MyBundle\Entity\BusTicket", inversedBy="customInfos")
* #ORM\JoinColumn(name="id_bus_ticket", referencedColumnName="id_bus_ticket", nullable=false)
*/
private $busTicket;
The answer is in your error message. You either have to add cascade={"persist"} to your entity annotation, or explicitly call persist. I don't believe you need em->merge() in this situation as you're never taking the entities out of context.
Where you have all your persist lines commented out, just try putting this in
$this->em->persist($busTicket);
$this->em->persist($busReservationDetail);
$this->em->persist($customInfo);
and if you're looping through a ton of entities, you could try adding the flush inside the loop at the end instead of a huge flush at the end.

Symfony 4: remove collection from entity

I have a product entity and product image entity. I want to use soft delete on product entity only and make a delete on product image entity.
The soft delete works fine. When I delete the product, the deleted_at column is set to current time.
So I would like to delete product image when the deleted_at column is updated.
I was wondering if I can do it directly in entity class? and how?
Product entity where I try to make the collection delation in setDeletedAt function.
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\ProductRepository")
* #ORM\Table(name="product")
*/
class Product
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="App\Entity\ProductImage", mappedBy="product", orphanRemoval=true, cascade={"persist"})
*/
private $productImages;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $deleted_at;
public function __construct()
{
$this->productImages = new ArrayCollection();
}
public function setDeletedAt(?\DateTimeInterface $deleted_at): self
{
// Here I try to remove images when deleted_at column is updated
$productImage = $this->getProductImages();
$this->removeProductImage($productImage);
$this->deleted_at = $deleted_at;
return $this;
}
/**
* #return Collection|ProductImage[]
*/
public function getProductImages(): Collection
{
return $this->productImages;
}
public function addProductImage(ProductImage $productImage): self
{
if (!$this->productImages->contains($productImage)) {
$this->productImages[] = $productImage;
$productImage->setProduct($this);
}
return $this;
}
public function removeProductImage(ProductImage $productImage): self
{
if ($this->productImages->contains($productImage)) {
$this->productImages->removeElement($productImage);
// set the owning side to null (unless already changed)
if ($productImage->getProduct() === $this) {
$productImage->setProduct(null);
}
}
return $this;
}
}
But when I make the soft delete, setDeletedAt() is called and the following error is returned:
Argument 1 passed to App\Entity\Product::removeProductImage() must be an instance of App\Entity\ProductImage, instance of Doctrine\ORM\PersistentCollection given, called in ...
Thanks for your help!
---- UPDATE ----
Solution provided by John works fine:
foreach ($this->getProductImages() as $pi) {
$this->removeProductImage($pi);
}
Thanks!
pretty self-explaining error:
at this point:
$productImage = $this->getProductImages();
$this->removeProductImage($productImage);
you are passing a collection instead a single ProductImage object.
to delete them all, just do:
foreach ($this->getProductImages() as $pi) {
$this->removeProductImage($pi);
}

Doctrine flush does not update related collection entity

I have some functionality that changes each element in collection of entity:
foreach ($screen->getBlocks() as $block) {
$block->setSomeField();
}
After that I'm trying to persist object in database:
$this->em->persist($screen);
$this->em->flush();
The Screen::$blocks property has annotations:
/**
* #ORM\OneToMany(targetEntity="App\Entity\Block", mappedBy="screen", cascade={"remove", "persist"}, orphanRemoval=true)
* #ORM\OrderBy({"position": "ASC"})
* #Groups({"block"})
* #ApiSubresource
* #Assert\Valid
*
* #var Block[]|Collection
*/
private $blocks;
Before flush I see that objects in collection are changed, but after - there values return back, seems that entityManager grabs if from database again. The only one solution that worked for me is adding $this->em->clear(); before flush, but I can't understand logic..
Can you try moving the persist over the foreach loop like this:
$this->em->persist($screen);
foreach ($screen->getBlocks() as $block) {
$block->setSomeField();
}
$this->em->flush();
try following method and look different
dump($screen->getBlocks());
foreach ($screen->getBlocks() as $block) {
$block->setSomeField();
dump(block);
}
dump($screen->getBlocks());

Symfony OneToMany - ManyToOne entity association not working

I'm training myself on Symfony and struggling with a problem with bidirectional association (very basic) because by dumping my entity in a twig template I verify that data is correct but the association is always null.
My problem is like this one but the solution is not shared.
I read the documentation here and it seems I follow the right steps.
My db contain a Parent table and a Children table related by children.parent_id as foreign key, both table are popolated and I use DOCTRINE:GENERATE:ENTITIES and DOCTRINE:GENERATE:CRUD.
In Parents class I have:
function __construct() {
$this->lastUpd = new \DateTime();
$this->children = new ArrayCollection();
}
/*
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Children", mappedBy="parent_id", cascade={"persist"})
*/
private $children;
public function setChildren(ArrayCollection $children) {
return $this->children = $children;
}
public function getChildren() {
return $this->children;
}
In Children class I have:
/**
* #var \AppBundle\Entity\Parents
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Parents", inversedBy="children")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="parent_id", referencedColumnName="parent_id")
* })
*/
private $parent_id;
/**
* Set parent_id
* #param \AppBundle\Entity\Parents $parent_id
* #return Parents
*/
public function setParentID(\AppBundle\Entity\Parents $parent_id= null) {
$this->parent_id = $parent_id;
return $this;
}
/**
* Get parent_id
* #return \AppBundle\Entity\Parents
*/
public function getParentID() {
return $this->parent_id;
}
As additional info looking at Simfony profiler (of parents list page) -> Doctrine -> Entities Mapping I found (with no errors) AppBundle\Entity\Parents and AppBundle\Entity\Type (a working unidirectional OneToMany association).
I am sorry to post a so basic error and I bet the solution is simple but I can't see it.
note: Im assuming that youre not creating an ArrayCollection of children and adding them en'mass.
you dont have any addChild method (which you need to call).
this is easy with an ArrayCollection.
public function addChild(Children $child) {
$this->children->add($child);
}
you could also do with a removeChild as well.
public function removeChild(Children $child) {
$this->children->removeElement($child);
}
then when in your controller.
$child = new Children();
$parent->addChild($child);
then when you persist the parent object, the children will follow due to the cascade persist. I would also add cascade={"remove"} as well, so when you delete the parent, the children will go to.

Symfony 2 - ManyToOne Bidirectional relationship behaviour

I had a big time trying to figure out how to setup a ManyToOne -> OneToMany relationship with Doctrine 2 and it still not working...
Here is the application behaviour:
A site has Pages
A User can write Comment on a Page
Here are my Entities (simplified):
Comment Entity:
**
* #ORM\Entity
* #ORM\Table(name="comment")
*/
class Comment {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* Many Comments have One User
*
* #ORM\ManyToOne(targetEntity="\Acme\UserBundle\Entity\User", inversedBy="comments")
*/
protected $user;
/**
* Many Comments have One Page
*
* #ORM\ManyToOne(targetEntity="\Acme\PageBundle\Entity\Page", inversedBy="comments")
*/
protected $page;
...
/**
* Set user
*
* #param \Acme\UserBundle\Entity\User $user
* #return Comment
*/
public function setUser(\Acme\UserBundle\Entity\User $user)
{
$this->user = $user;
return $this;
}
/**
* Set page
*
* #param \Acme\PageBundle\Entity\Page $page
* #return Comment
*/
public function setPage(\Acme\PageBundle\Entity\Page $page)
{
$this->page = $page;
return $this;
}
User Entity:
/**
* #ORM\Entity
* #ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* The User create the Comment so he's supposed to be the owner of this relationship
* However, Doctrine doc says: "The many side of OneToMany/ManyToOne bidirectional relationships must be the owning
* side", so Comment is the owner
*
* One User can write Many Comments
*
* #ORM\OneToMany(targetEntity="Acme\CommentBundle\Entity\Comment", mappedBy="user")
*/
protected $comments;
...
/**
* Get Comments
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getComments() {
return $this->comments ?: $this->comments = new ArrayCollection();
}
Page Entity:
/**
* #ORM\Entity
* #ORM\Table(name="page")
*/
class Page
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* One Page can have Many Comments
* Owner is Comment
*
* #ORM\OneToMany(targetEntity="\Acme\CommentBundle\Entity\Comment", mappedBy="page")
*/
protected $comments;
...
/**
* #return \Doctrine\Common\Collections\Collection
*/
public function getComments(){
return $this->comments ?: $this->comments = new ArrayCollection();
}
I want a bidirectional relationship to be able to get the collection of Comments from the Page or from the User (using getComments()).
My problem is that when I try to save a new Comment, I get an error saying that doctrine is not able to create a Page entity. I guess this is happening because it's not finding the Page (but it should) so it's trying to create a new Page entity to later link it to the Comment entity that I'm trying to create.
Here is the method from my controller to create a Comment:
public function createAction()
{
$user = $this->getUser();
$page = $this->getPage();
$comment = new EntityComment();
$form = $this->createForm(new CommentType(), $comment);
if ($this->getRequest()->getMethod() === 'POST') {
$form->bind($this->getRequest());
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$comment->setPage($page);
$comment->setUser($user);
$em->persist($comment);
$em->flush();
return $this->redirect($this->generateUrl('acme_comment_listing'));
}
}
return $this->render('AcmeCommentBundle:Default:create.html.twig', array(
'form' => $form->createView()
));
}
I don't understand why this is happening. I've checked my Page object in this controller (returned by $this->getPage() - which return the object stored in session) and it's a valid Page entity that exists (I've checked in the DB too).
I don't know what to do now and I can't find anyone having the same problem :(
This is the exact error message I have:
A new entity was found through the relationship
'Acme\CommentBundle\Entity\Comment#page' that was not configured to
cascade persist operations for entity:
Acme\PageBundle\Entity\Page#000000005d8a1f2000000000753399d4. To solve
this issue: Either explicitly call EntityManager#persist() on this
unknown entity or configure cascade persist this association in the
mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot
find out which entity causes the problem implement
'Acme\PageBundle\Entity\Page#__toString()' to get a clue.
But I don't want to add cascade={"persist"} because I don't want to create the page on cascade, but just link the existing one.
UPDATE1:
If I fetch the page before to set it, it's working. But I still don't know why I should.
public function createAction()
{
$user = $this->getUser();
$page = $this->getPage();
// Fetch the page from the repository
$page = $this->getDoctrine()->getRepository('AcmePageBundle:page')->findOneBy(array(
'id' => $page->getId()
));
$comment = new EntityComment();
// Set the relation ManyToOne
$comment->setPage($page);
$comment->setUser($user);
$form = $this->createForm(new CommentType(), $comment);
if ($this->getRequest()->getMethod() === 'POST') {
$form->bind($this->getRequest());
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($comment);
$em->flush();
return $this->redirect($this->generateUrl('acme_comment_listing'));
}
}
return $this->render('AcmeCommentBundle:Default:create.html.twig', array(
'form' => $form->createView()
));
}
UPDATE2:
I've ended up storing the page_id in the session (instead of the full object) which I think is a better idea considering the fact that I won't have a use session to store but just the id. I'm also expecting Doctrine to cache the query when retrieving the Page Entity.
But can someone explain why I could not use the Page entity from the session? This is how I was setting the session:
$pages = $site->getPages(); // return doctrine collection
if (!$pages->isEmpty()) {
// Set the first page of the collection in session
$session = $request->getSession();
$session->set('page', $pages->first());
}
Actually, your Page object is not known by the entity manager, the object come from the session. (The correct term is "detached" from the entity manager.)
That's why it tries to create a new one.
When you get an object from different source, you have to use merge function. (from the session, from an unserialize function, etc...)
Instead of
// Fetch the page from the repository
$page = $this->getDoctrine()->getRepository('AcmePageBundle:page')->findOneBy(array(
'id' => $page->getId()
));
You can simply use :
$page = $em->merge($page);
It will help you if you want to work with object in your session.
More information on merging entities here

Resources