jdog jdog - 1 month ago 6
PHP Question

doctrine entity manager duplicate insert on update

I have build a cli application with Symfony full stack that processes XLSX files. However, updating information in a database record at 2 different points leads to the record being duplicated instead of updated.

A most simple breakdown of the application is:

Command

class AppProcessFilesCommand extends ContainerAwareCommand
{

protected function execute(InputInterface $input, OutputInterface $output)
{
$em = $this-setContainer>getContainer()->get('doctrine')->getManager();
$file = $em->getRepository( 'AppBundle:FileToSync' )->findBy(
['processed' => null],
['modified' => 'ASC']
);
if (sizeof($file) > 0) {
$file = $file[0];

foreach (ProcessorFactory::getAvailableProcessors() as $processor) {
$start = microtime( true );
if (ProcessorFactory::getInstance( $processor )
->setOutput( $output )
->setContainer( $this->getContainer() )
->setDoctrine( $this->getContainer()->get( 'doctrine' ) )
->process( $file )
) {
$processorFound = true;
$file->setTimeTaken( microtime( true ) - $start );
$file->setProcessed( new \DateTime() );
$em->persist($file);
$em->flush();
}
}


Processing loop

class Processor
{
public function process($fileToSync)
{
$foundFiles = $this->convertToCsv($file);
$noRows = $this->processCsvSheets($foundFiles, $fileToSync);

$em = $this->getDoctrine()->getManager();
$fileToSync->setDetectedTypeId($this->getMyFileTypeId());
$fileToSync->setRowCount($noRows);
$em->persist($fileToSync);
$em->flush();


Entity class

namespace AppBundle\Entity;


class FileToSync
{
private $id;

private $absolute_path;

private $modified;

private $processed;

/**
* @var int
*/
private $detected_type_id;

private $time_taken;

private $row_count;

/**
* @var \AppBundle\Entity\DetectedType
*/
private $DetectedType;


/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}

/**
* Set absolutePath
*
* @param string $absolutePath
*
* @return FileToSync
*/
public function setAbsolutePath($absolutePath)
{
$this->absolute_path = $absolutePath;

return $this;
}

/**
* Get absolutePath
*
* @return string
*/
public function getAbsolutePath()
{
return $this->absolute_path;
}

/**
* Set modified
*
* @param \DateTime $modified
*
* @return FileToSync
*/
public function setModified($modified)
{
$this->modified = $modified;

return $this;
}

/**
* Get modified
*
* @return \DateTime
*/
public function getModified()
{
return $this->modified;
}

/**
* Set detectedTypeId
*
* @param \integer $detectedTypeId
*
* @return FileToSync
*/
public function setDetectedTypeId($detectedTypeId)
{
$this->detected_type_id = $detectedTypeId;

return $this;
}

/**
* Get detectedTypeId
*
* @return \integer
*/
public function getDetectedTypeId()
{
return $this->detected_type_id;
}

/**
* Set processed
*
* @param \datetime $processed
*
* @return FileToSync
*/
public function setProcessed(\datetime $processed)
{
$this->processed = $processed;

return $this;
}

/**
* Get processed
*
* @return \datetime
*/
public function getProcessed()
{
return $this->processed;
}

/**
* Set detectedType
*
* @param \AppBundle\Entity\DetectedType $detectedType
*
* @return FileToSync
*/
public function setDetectedType(\AppBundle\Entity\DetectedType $detectedType = null)
{
$this->DetectedType = $detectedType;

return $this;
}

/**
* Get detectedType
*
* @return \AppBundle\Entity\DetectedType
*/
public function getDetectedType()
{
return $this->DetectedType;
}

/**
* Set timeTaken
*
* @param string $timeTaken
*
* @return FileToSync
*/
public function setTimeTaken($timeTaken)
{
$this->time_taken = $timeTaken;

return $this;
}

/**
* Get timeTaken
*
* @return string
*/
public function getTimeTaken()
{
return $this->time_taken;
}

/**
* Set rowCount
*
* @param integer $rowCount
*
* @return FileToSync
*/
public function setRowCount($rowCount)
{
$this->row_count = $rowCount;

return $this;
}

/**
* Get rowCount
*
* @return integer
*/
public function getRowCount()
{
return $this->row_count;
}
}


Entity Mapping (yml)

AppBundle\Entity\DetectedType:
type: entity
table: detected_type
id:
id:
type: integer
generator: { strategy: AUTO }
fields:
name:
type: string
length: 501

AppBundle\Entity\FileToSync:
type: entity
table: file_to_sync
id:
id:
type: integer
generator: { strategy: AUTO }
manyToOne:
DetectedType:
targetEntity: DetectedType
joinColumn:
name: detected_type_id
referencedColumnName: id
fields:
absolute_path:
type: string
length: 255
modified:
type: datetime
detected_type_id:
type: integer
nullable: true
processed:
type: datetime
nullable: true
time_taken:
type: decimal
precision: 11
scale: 6
nullable: true
row_count:
type: integer
nullable: true


AppBundle\Entity\Transaction:
type: entity
table: transaction
id:
id:
type: integer
generator: { strategy: AUTO }
uniqueConstraints:
txnId:
columns: [ txn_id ]
manyToOne:
FileToSync:
targetEntity: FileToSync
joinColumn:
name: file_id
referencedColumnName: id
fields:
txnDate:
type: datetime
file_id:
type: integer


In the processing loop, $fileToSync is not updated, but rather a new record inserted. This is then updated in the command.

I am working under the assumption that $this->getContainer()->get('doctrine')->getManager(); works as a Singleton?

Answer

Yes, by default symfony2 services work like singleton, you can read this:

http://symfony.com/doc/2.6/cookbook/service_container/scopes.html

Understanding Scopes¶

The scope of a service controls how long an instance of a service is used by the container. The Dependency Injection component provides two generic scopes:

container (the default one): The same instance is used each time you request it from this container. prototype: A new instance is created each time you request the service

Firsly, you can not use persist method if your 'file' entity was loaded from entityManager. Second dump 'file' entity before flush and check what you try to save. Also you can check entity status at UnitOfWork here sample:

$unitOfWork = $entityManager->getUnitOfWork();

foreach ($unitOfWork->getScheduledEntityInsertions() as $entity) {
   #for insert
}

foreach ($unitOfWork->getScheduledEntityUpdates() as $entity) {
    # for update
}

The semantics of the persist operation, applied on an entity X, are as follows:

If X is a new entity, it becomes managed. The entity X will be entered into the database as a result of the flush operation. If X is a preexisting managed entity, it is ignored by the persist operation. However, the persist operation is cascaded to entities referenced by X, if the relationships from X to these other entities are mapped with cascade=PERSIST or cascade=ALL (see “Transitive Persistence”). If X is a removed entity, it becomes managed. If X is a detached entity, an exception will be thrown on flush.

I thought you are mapping entity in class, but you are not and i can't understand which relation you are using with 'DetectedType'. This problem might happen if you call 'clear' method somewhere or if your have problem with relation owning side, please read this:

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/unitofwork-associations.html

Comments