Reputation: 5899
I tried to implement a versionable ORM using Doctrine2.
Everything works well. When I update an existing entry the old entry gets inserted into the *_version table.
But when I update the entry in another request (and therefor a new instance of EntityManager
) the old entry won't be written anymore to the *_version
table although the entry in the basic table gets updated without any problems (even the version no. gets incremented by 1).
I like to show you my very simple versionable ORM:
UPDATE: The example code below works now!
Also check my Gist with the logEntityVersion
helper method.
ProductBase.php
trait ProductBase
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* @ORM\Column(type="string")
*/
protected $name;
// getters and setters here
}
Product.php
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* Product
*
* @ORM\Entity
* @ORM\Table(name="product")
* @ORM\HasLifeCycleCallbacks
*/
class Product
{
use ProductBase;
/**
* @ORM\OneToMany(targetEntity="ProductVersion", mappedBy="product", cascade={"persist"})
*/
private $auditLog;
/**
* @ORM\Column(type="integer")
* @ORM\Version
*/
private $version = 1;
public function __construct()
{
$this->auditLog = new ArrayCollection();
}
public function logVersion()
{
echo sprintf("Creating a new version for ID %s, version %s\n", $this->getId(), $this->getVersion());
$this->auditLog[] = new ProductVersion($this);
}
// Version getter and setter
}
ProductVersion.php
use Doctrine\ORM\Mapping as ORM;
/**
* ProductVersion
*
* @ORM\Entity
* @ORM\Table(name="product_version")
*/
class ProductVersion
{
use ProductBase;
/**
* @ORM\ManyToOne(targetEntity="Product", inversedBy="auditLog")
*/
private $product;
/**
* @ORM\Column(type="integer")
*/
private $version;
public function __construct(Product $product)
{
$this->product = $product;
$this->name = $product->getName();
$this->version = $product->getVersion();
var_dump($product->getVersion());
}
// Version getter and setter
}
And this is the code for inserting a new entry and update it to create a new version:
// Insert new product
$this->entityManager->beginTransaction();
$this->entityManager->flush();
$product = new Product();
$product->setName('Product V1');
$this->entityManager->persist($product);
$this->entityManager->flush();
$this->entityManager->commit();
$productId = $product->getId();
echo "Created Product with ID " . $product->getId() . "\n";
/** @var Product $foo */
$foo = $this->entityManager->getRepository('orm\Product')->find($productId);
// Log version (Product V1)
$this->entityManager->beginTransaction();
$this->entityManager->flush();
$foo->logVersion();
$this->entityManager->flush();
$this->entityManager->commit();
// Update 1
$foo->setName('Product V2');
$this->entityManager->flush();
// Log version (Product V2)
$this->entityManager->beginTransaction();
$this->entityManager->flush();
$foo->logVersion();
$this->entityManager->flush();
$this->entityManager->commit();
// Update 2
$foo->setName('Product V3');
$this->entityManager->flush();
Schema generation
$tools = new SchemaTool($this->entityManager);
var_dump($tools->getCreateSchemaSql(array(
$this->entityManager->getClassMetadata('orm\Product'),
$this->entityManager->getClassMetadata('orm\ProductVersion')
)));
Upvotes: 3
Views: 3415
Reputation: 21817
I see a couple of issues with your code, and unfortunately the concept as well:
You've defined Product::$auditLog
as an array, but it needs to be a Collection. Use this in stead:
class Product
{
private $auditLog;
public function __construct()
{
$this->auditLog = new \Doctrine\Common\Collections\ArrayCollection();
}
You've defined Product::$auditLog
as being mapped by ProductVersion::$product
, but haven't defined ProductVersion::$product
as being inversed by Product::$auditLog
. Fix it like this:
class ProductVersion
{
/** @ORM\ManyToOne(targetEntity="Product", inversedBy="auditLog") */
private $product;
Also, please validate your mappings and database schema. Every error might lead to unexpected results.
$ doctrine orm:validate-schema // plain Doctrine2
$ app/console doctrine:schema:validate // Symfony2
You're using the @Version
annotation in the trait, meaning both Product
and ProductVersion
will be versioned by Doctrine. As @Version
is used for optimistic locking, I doubt this is what you're really after.
Records in an audit-log should never be updated (or deleted), only added. So locking records doesn't really make sense here.
Please remove ProductBase::$version
and in stead add:
Product::$version
with @Version
annotation.ProductVersion::$version
without @Version
annotation.Now only Product
will be versioned by Doctrine, ProductVersion
will simply contain the version-number of the product that is logged.
You're using @PrePersist
and @PreUpdate
lifecycle callbacks to populate the audit-log. @PreUpdate
is going to be a problem, as the docs state:
Changes to associations of the updated entity are never allowed in this event
Yet you are changing the Product::$auditLog
association by adding a record to the collection.
You'll have to move this logic, and there are many options as to where to:
One option could be to start a transaction manually, flush (and keep track of versioned entities), create log entries, flush again, commit the transaction. Or you could replace the second flush with direct inserts using the DBAL connection.
Upvotes: 4