Doctrine merging entity with unidirectional OneToMany not clearing database entries - php

First off, I use Doctrine v2.6.2 with Symfony v4.1.7.
I have an entity Product which (among others) has a unidirectional one-to-many relation with another entity AlternativeDuration. Following Doctrine's documentation, the mapping in my product class looks like this:
/**
* #ORM\ManyToMany(
* targetEntity="AlternativeDuration",
* cascade={"persist", "merge", "remove"},
* orphanRemoval=true
* )
* #ORM\JoinTable(name="ProductAlternativeDurations",
* joinColumns={#ORM\JoinColumn(name="product_id", referencedColumnName="id", onDelete="CASCADE", nullable=false)},
* inverseJoinColumns={#ORM\JoinColumn(name="alternative_duration_id", referencedColumnName="id", unique=true, onDelete="CASCADE", nullable=false)}
* )
*/
protected $alternativeDurations;
My application recently started using React, this means I now submit a JSON representation of my product (along with an array of alternative durations) which I need to deserialize into the Product entity in the back-end. I use the JMS serializer with default configuration for this.
Now the problem I'm having happens when editing an existing product, the product already has an alternative duration which I delete. The submitted JSON looks like this:
{
"id": 1, # the ID of the existing product here
"alternativeDurations": [] # empty array because the entry is removed
}
In the back-end I successfully deserialize the JSON string:
$serializedProduct = $this->serializer->deserialize($jsonString, Product::class, 'json');
I verified here that the $serializedProduct has no alternative durations. Then I follow with a merge + flush. I expect the merge to fetch the existing product and supplement it with the $serializedProduct.
$em->merge($serializedProduct); # $em being the EntityManager.
$em->flush();
Now I would expect the AlternativeDuration entry, along with the ProductAlternativeDurations join table entry being removed. The result, however, is that the entry in ProductAlternativeDurations is removed but the AlternativeDuration is still there.
I'm at a loss now, anyone can give some pointers on why the AlternativeDuration entry is not deleted?
EDIT 19-11-2018:
It seems this is a known bug in Doctrine: #2542
Also merge will be removed in Doctrine3 so I will probably need to rethink this approach in general.

Related

Doctrine prevent object deletion

In my project I have two entities: planifications and selections.
There is a relation between these two objects: A planification MUST contain ONE selection. The same selection can be used by multiple planifications.
The generated code looks like this:
// Planification.php - class Planification
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Selection", inversedBy="planifications")
* #ORM\JoinColumn(name="selection_id", referencedColumnName="id")
*/
private $selection;
// Selection.php - class Selection
/**
* #ORM\OneToMany(targetEntity="App\Entity\Planification", mappedBy="selection")
*/
private $planifications;
What I would like to do is not allow a selection to be deleted if it is referenced by a planification. In other words, if a planification contains a selection - that selection can not be deleted. What happens to me is if I try to delete a selection that is in a planification, the operation completes successfully, and the $selection member in the Planification class contains NULL.
Would fixing this be possible in doctrine? I have tried adding nullable=false (on the $selection member) and onDelete="NO ACTION", and both solutions don't work.
The correct Doctrine annotation to disallow Planification::$selection to be null, would be:
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Selection", inversedBy="planifications")
* #ORM\JoinColumn(name="selection_id", nullable=false)
*/
private $selection;
(You do not need the referencedColumnName setting, since it defaults to id, and nullable=false goes in the #JoinColumn annotation).
Having the annotation will not update the DB to fit this particular definition.
Execute bin/console doctrine:schema:update --dump-sql to see the needed SQL to update your table definition, and run the resultant appropriate SQL statements against your DB to update the DB schema.

Doctrine Results Missing Entity

I've been trying to figure this out for a while now. Let's start with the basic information, I have a client table and a contact table. The client table has a OneToMany and OneToOne relation with contact
class Client
{
/**
* #var int
* #Id
* #Column(type="integer", nullable=false, unique=true, options={"comment":"Auto incrementing client_id of each client"})
* #GeneratedValue
*/
protected $pid;
/**
* #OneToMany(targetEntity="Contact", mappedBy="client")
* #JoinColumn(name="contact_id", referencedColumnName="pid")
* #var Contact[]
*/
protected $contact;
/**
* #OneToOne(targetEntity="Contact")
* #JoinColumn(name="defaultcontact_id", referencedColumnName="pid", nullable=true)
* #var Contact
*/
protected $default_contact;
The contact table has a ManyToOne relation with Client:
class Contact
{
/**
* #var int
* #Id
* #Column(type="integer", nullable=false, unique=true, options={"comment":"Auto incrementing user_id of each user"})
* #GeneratedValue
*/
protected $pid;
/**
* #ManyToOne(targetEntity="Client", inversedBy="contact")
* #JoinColumn(name="client_id", referencedColumnName="pid")
*/
protected $client;
Here's the query that I've been using:
$qb = $entityManager->createQueryBuilder();
$qb->select("cn as contact", "cl as client")
->from('DB\Contact', 'cn')
->innerJoin('cn.client', 'cl')
->where(
$qb->expr()->andX(
$qb->expr()->eq('cl.client_name', '?1'),
$qb->expr()->eq('cn.pid', '?2')
)
)
->setParameter(1, $client)
->setParameter(2, $contact);
try
{
$result = $qb->getQuery()->getOneOrNullResult();
}
I want both the contact and the client. And this is where I'm having problems: array_keys($result) ends up outputting:
Array
(
[0] => contact
)
I wanted something like this:
[0] => contact
[1] => client
In other words, the Client Entity is missing. Flipping the SELECT FROM from the Contact to the Client repository yielded the reverse situation, contact was missing.
I've checked over the previous code, while entityManager was reused from the login step, this is the first time the Client and Contact repository are accessed so I don't believe it's a caching problem.
Here's the SQL statement being executed:
Executing SQL:
SELECT c0_.pid AS pid0, c0_.caller AS caller1, c0_.address_1 AS address_12, c0_.address_2 AS address_23,
c0_.unit AS unit4, c0_.city AS city5, c0_.state AS state6, c0_.zip_code AS zip_code7, c0_.phone AS phone8,
c0_.email AS email9, c0_.is_active AS is_active10, c0_.date_created AS date_created11,
c0_.date_last_modified AS date_last_modified12, c1_.pid AS pid13, c1_.client_name AS client_name14,
c1_.is_active AS is_active15, c1_.date_created AS date_created16, c1_.date_last_modified AS date_last_modified17,
c0_.client_id AS client_id18, c0_.created_by_id AS created_by_id19, c0_.last_modified_by_id AS last_modified_by_id20,
c1_.defaultcontact_id AS defaultcontact_id21, c1_.created_by_id AS created_by_id22,
c1_.last_modified_by_id AS last_modified_by_id23
FROM contacts c0_
INNER JOIN clients c1_ ON c0_.client_id = c1_.pid
WHERE c1_.client_name= ? AND c0_.pid = ?
As a sidenote, if I alter the select so that the missing entity accesses a specific column, I'll get the desired values.
e.g.
$qb->select("cn as contact", "cl.pid as client")
->from('RGAServ\DB\Contact', 'cn')
will have the following array_keys($result):
Array
(
[0] => contact
[1] => client
)
So I can assure you that the client does exist in the database and it should be properly attached to the contact, it's just that under the first select statement where I want the whole entity and not just one column, the entity ends up not being pushed into the result array.
Why is this? Are there too many columns in the Sql statement? Am I forgetting something in the annotations?
First: You can't have different entities in the resulting array: every row in the result must be in the same format.
Second: If you examine your SQL query closely you'll notice that the one row that is returned contains both the contact (c0_) and the client (c1_).
Try executing the SQL query in the database to see the result.
After looking through a bunch of stack overflow questions, I've come to the following conclusion: It looks like this is how doctrine handles "Fetch Joins".
The big clue comes from here:
Doctrine Regular vs Fetch join
With supporting evidence for this behavior's existence coming from:
Doctrine - entities not being fetched
Doctrine join bypass lazy loading
Doctrine2 query with select on multiple entities from different Symfony2 bundles
More specifically, this quote from the doctrine documentation starts to make sense (http://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html#joins):
When Doctrine hydrates a query with fetch-join it returns the class in
the FROM clause on the root level of the result array. In the previous
example an array of User instances is returned and the address of each
user is fetched and hydrated into the User#address variable. If you
access the address Doctrine does not need to lazy load the association
with another query.
In layman's terms, during a FETCH JOIN, contact (or client if I'm SELECTing FROM the client repository) is designated as the root entity. Whatever is found for Client will then be pushed into the contact's $client variable to be retrieved afterwards using a getter accessor.
The retrieval itself will not need a follow-up database query to fetch the client entity. I'll need to do a little testing, but it looks like this behavior was for the situation when multiple results are returned during the join. Instead of cluttering up the results, they're organized under an intuitive location.
In other words, I had the wrong expectations and was looking in the wrong spot. The client entity did indeed come back, but it wasn't placed in results. It was filed under contact. Retrieving it separately is, therefore, a given but at least it won't need another database call.
At least now, I believe I know why when I had Client in the from field, I was getting one specific contact instead of all of them when I tried to use the getContact() accessor.

Doctrine 2 - How to use objects retrieved from cache in relationships

I'm working in a project that use Doctrine 2 in Symfony 2 and I use MEMCACHE to store doctrine's results.
I have a problem with objects that are retrieved from MEMCACHE.
I found this post similar, but this approach not resolves my problem: Doctrine detaching, caching, and merging
This is the scenario
/**
* This is in entity ContestRegistry
* #var contest
*
* #ORM\ManyToOne(targetEntity="Contest", inversedBy="usersRegistered")
* #ORM\JoinColumn(name="contest_id", referencedColumnName="id", onDelete="CASCADE"))
*
*/
protected $contest;
and in other entity
/**
* #var usersRegistered
*
* #ORM\OneToMany(targetEntity="ContestRegistry", mappedBy="contest")
*
*/
protected $usersRegistered;
Now imagine that Contest is in cache and I want to save a ContestRegistry entry.
So I retrieve the object contest in cache as follows:
$contest = $cacheDriver->fetch($key);
$contest = $this->getEntityManager()->merge($contest);
return $contest;
And as last operation I do:
$contestRegistry = new ContestRegistry();
$contestRegistry->setContest($contest);
$this->entityManager->persist($contestRegistry);
$this->entityManager->flush();
My problem is that doctrine saves the new entity correctly, but also it makes an update on the entity Contest and it updates the column updated. The real problem is that it makes an update query for every entry, I just want to add a reference to the entity.
How I can make it possible?
Any help would be appreciated.
Why
When an entity is merged back into the EntityManager, it will be marked as dirty. This means that when a flush is performed, the entity will be updated in the database. This seems reasonable to me, because when you make an entity managed, you actually want the EntityManager to manage it ;)
In your case you only need the entity for an association with another entity, so you don't really need it to be managed. I therefor suggest a different approach.
Use a reference
So don't merge $contest back into the EntityManager, but grab a reference to it:
$contest = $cacheDriver->fetch($key);
$contestRef = $em->getReference('Contest', $contest->getId());
$contestRegistry = new ContestRegistry();
$contestRegistry->setContest($contestRef);
$em->persist($contestRegistry);
$em->flush();
That reference will be a Proxy (unless it's already managed), and won't be loaded from the db at all (not even when flushing the EntityManager).
Result Cache
In stead of using you own caching mechanisms, you could use Doctrine's result cache. It caches the query results in order to prevent a trip to the database, but (if I'm not mistaken) still hydrates those results. This prevents a lot of issues that you can get with caching entities themselves.
What you want to achieve is called partial update.
You should use something like this instead
/**
* Partially updates an entity
*
* #param Object $entity The entity to update
* #param Request $request
*/
protected function partialUpdate($entity, $request)
{
$parameters = $request->request->all();
$accessor = PropertyAccess::createPropertyAccessor();
foreach ($parameters as $key => $parameter) {
$accessor->setValue($entity, $key, $parameter);
}
}
Merge requires the whole entity to be 100% fullfilled with data.
I haven't checked the behavior with children (many to one, one to one, and so on) relations yet.
Partial update is usually used on PATCH (or PUT) on a Rest API.

Correct Symfony-doctrine inheritance mapping

I recently started using Symfony2-Doctrine2. I'm not getting how to save data in inheritance mapping.
My requirements:
For learning exercise:
I'm making a library application for testing (Requirements might not be practical).
At high level, library may contain many different type of items like books, articles, manuals as example for now.
They have some common fields like name, publish year etc and some item specific details like book has IDBN, publisher; Manual have company, product.
Again to make problem little more complex, there is another 'item_content' table to have some description in different language.
To quickly visualize, I've following structure:
I achieved above structure as per doctrine docs for inheritance mapping & Bidirectional one to many relation
My Question: How to save data using Symfony2 (I've proper routing/actions running, just need code to write in controller or better in repository). While saving data (say for manual) I want to save data in Item, Manual and ItemContect table but getting confused due to discr field in database. I didn't find code for saving data in above structure. I don't need full code, just few hints will be sufficient. My Item class is as follow (Other classes have proper inverse as mentioned in doctrine docs):
/**
* Article
*
* #ORM\Table(name="item")
* #ORM\Entity(repositoryClass="Test\LibraryBundle\Entity\ItemRepository")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"book" = "Book", "manual" = "Manual", "article" = "Article"})
*/
class Item
{
//...
/**
* For joining with ItemContent
*
* #ORM\OneToMany(targetEntity="ItemContent", mappedBy="item")
**/
private $itemContents;
public function __construct()
{
$this->itemContents = new ArrayCollection();
}
//...
}
The discriminator field will be automatically filled by Doctrine
$em = $this->getDoctrine()->getManager();
$item = new Manual(); // discr field = "manual"
$itemContent = new ItemContent();
$item->addItemContent($itemContent);
$itemContent->setItem($item);
$em->persist($item);
$em->persist($itemContent);
$em->flush();
Is that the answer you're waiting ?

Symfony2 entity field in a form

I'm trying to create a simple form to add companies and i've a trouble using an entity.
I added a select field using a company type entity :
->add('idtypesociete', 'entity', array('class' => 'PromocastUtilisateurBundle:PcastTypesociete', 'property' => 'nomtypesociete'))
But when i submit the form my idtypesociete field contain an 'PcastTypesociete' object and not just the value of the option selected. So the submission fail.
I made a Many-To-One relation between my company entity and my typeCompany entity like this:
/**
* #var integer $idtypesociete
*
* #ORM\Column(name="IDTYPESOCIETE", type="integer", nullable=false)
* #ORM\ManyToOne(targetEntity="Promocast\UtilisateurBundle\Entity\PcastTypesociete")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="PcastTypesociete_idtypesociete", referencedColumnName="idtypesociete")
* })
*/
private $idtypesociete;
Do you have a solution to get only the id of the company type selected? (if possible without made a simple sql request to list my companies types)
Thanks a lot !
If the relationships are working then Symfony 2 usually does a very good job of building the form fields for you.
I think the issue is the $idtypesociete property. Are you expecting to store an integer here on the hydrated entity?
Doctrine associations use Entity relationships. The annotations you supply determine the behind-the-scenes stuff like the join column:
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html#many-to-one-unidirectional
I suggest backing up or committing your work before doing anything else.
Does changing the entity property to the following help?
/**
* #var PcastTypesociete $typesociete
*
* #ORM\Column(name="IDTYPESOCIETE", type="integer", nullable=false)
* #ORM\ManyToOne(targetEntity="Promocast\UtilisateurBundle\Entity\PcastTypesociete")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="PcastTypesociete_idtypesociete", referencedColumnName="idtypesociete")
* })
*/
private $typesociete;
You may need to update your database schema via doctrine:schema:update using the console if it doesn't work properly the first time. Your Entity will also need to be updated to reflect the new property name.
If that works then your form should only need ->add('typesociete') in the form type and you'll have a functioning entity select field because Symfony is clever enough to know what field type to use.

Categories