Orm: How to persist only one end of a relationship and the other one only if the entity doesn't exist?

Created on 19 Oct 2017  路  7Comments  路  Source: doctrine/orm

I have a complex/nested object created by automatic hydration from Zend\Form data. Now I want to save it with Doctrine 2. The best case would be just one persist(...) and one flush(...) call on the top level. But it doesn't work like this. So now I have following problem:

There are objects User and Order. The relationship is 1:n (so, 1 User has n Orders). The User exists already. When a User Joe tries to save more than one Order (e.g. its second order), an error occurs:

A new entity was found through the relationship '...\Order#user' that was not configured to cascade persist operations for entity: ...\User@000000003ba4559d000000005be8d831. 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 '...\User#__toString()' to get a clue.

Allright, I addeed cascade={"persist"} (though it doesn't make sense here, but anyway, just to try it out):

class Order
{
    ...
    /**
     * @var User
     *
     * @ORM\ManyToOne(targetEntity="User", cascade={"persist"})
     */
    protected $user;
    ...
}

Now it works, if the given User doesn't exist: An Order and a User is created.

But if the User exists, an error occurs:

An exception occurred while executing 'INSERT INTO user (username, role, created, updated) VALUES (?, ?, ?, ?)' with params ["myusername", "member", null, null]:

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'myusername' for key 'username_UNIQUE'

How to handle the saving so, that the User only gets saved, if it doesn't exist yet?

Actually I was sure, it's a standard problem, and I'm wondering, that I cannot find an answer on the net. I've also already posted a question on StackOverflow, but haven't got any answer.

Most helpful comment

@automatix do you use some serializer for creating instances of your entities?

All 7 comments

The solution is to persist the User before saving the referencing entity. If it doesn't exist yet, it needs to be created (persisted and flushed) first:

$user = $this->entityManager->getRepository(User::class)->findOneBy(
    ['username' => $dataObject->getUser()->getUsername()]
);                            
if (! $user) {                
    $this->entityManager->persist($dataObject->getUser());
    $this->entityManager->flush($dataObject->getUser());
    $user = $this->entityManager->getRepository(User::class)->findOneBy(
        ['username' => $dataObject->getUser()->getUsername()]
    );                        
}                             
$dataObject->setUser($user);  

$this->entityManager->persist($dataObject);
$this->entityManager->flush();

And the cascade={"persist"} should not be used, since it actually doesn't make sense in this case.

Or even easier:

$user = $this->entityManager->getRepository(User::class)->findOneBy(
    ['username' => $dataObject->getUser()->getUsername()]
);
if ($user) {
    $dataObject->setUser($user);
} else {
    $this->entityManager->persist($dataObject->getUser());
}

$this->entityManager->persist($dataObject);
$this->entityManager->flush();

@automatix do you use some serializer for creating instances of your entities?

Hello, I have the same issue but I create full object with JMSSerializer deserializing data I've got from external soap source. So I want to persist full object with relations.

I had exact the same issue.
My workaround was to fetch the offending entity using the appropriate repositories ->find($id) method.
Somehow the persistance layer did not recognize an entity fetched using eg. ->findOneBy($criteria) as existing entity and tried to persist a new version of it.

So this was my workaround, which fixed the problem imemdiately:

$userRepository = $this->em->getRepository(\FOS\UserBundle\Model\User::class);
$user = $userRepository->find($user->getId());

from here on I was able to set it as relation in other entities or persist it just for test

$this->objectManager->persist($user );
$this->objectManager->flush();

Somehow the persistance layer did not recognize an entity fetched using eg. ->findOneBy($criteria) as existing entity and tried to persist a new version of it.

This is likely a bug that needs to be inspected (with an integration test)

I found out what might be the cause of the buggy behaviour I described above.
I use FOSUserBundle and override the default user manager '@fos_user.user_manager.default' with my own version '@app.entity_manager.user'. This might cause some confusion which entity manager is responsible.
The persistence error occurs even after I checked my entire app and made sure that my own UserManager and my own UserManagerInterface is consistently used throughout my entire app.

Then accidentially, I removed a doctrine listener service which was using my user manager and the error was gone! :-) The service definition was this:

    App\EventSubscriber\Doctrine\UserSubscriber:
        tags:
            - { name: doctrine.event_subscriber }
        calls:
            - method: setUserManager
              arguments:
                  - '@app.entity_manager.user'

I removed the dependency on '@app.entity_manager.user' and it still works as expected.
At some point during my try&error I saw a "circular dependency" error. May be it was because of this doctrine listener?

my environment:
symfony/framework-bundle v4.2.4
friendsofsymfony/user-bundle dev-master ee76c57c6a0966c24
doctrine/orm v2.6.3

Was this page helpful?
0 / 5 - 0 ratings