· 7 years ago · Feb 16, 2019, 06:26 AM
1# Issue
2
3Projectors fail with contraint errors when rebuilding read models.
4
5## Setup description
6
7Implementations of Prooph\EventStore\Projection\AbstractReadModel are used to build read-models.
8We are using Doctrine Entities for these models.
9ReadModelProjector::OPTION_PERSIST_BLOCK_SIZE is set to 1000
10The persist method of Prooph\EventStore\Projection\AbstractReadModel has been adapted to wrap the stack of changes inside a transaction. (see ReadModelTrait.php)
11
12## The Problem
13
14In regular production this works fine, as the stack of changes is usually small since the projector is up-to-date.
15However when rebuilding the read models I reguarly and relyable get constraint exceptions.
16
17`SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '24a4e8ed-de31-5574-bed9-2da25ff29070' for key 'PRIMARY'`
18
19These errors seem to come from cascading one-2-many relationships.
20
21## Analises
22
23As said, these exceptions seem to occure for cascading one-2-many relationships. In our case a single Order may have many Properties. But only one of the same type (or name).
24
25For the appropiate events I determain if I should create, update or remove a property based on its name and existence on the order.
26
27The code for this typically looks like;
28
29```php
30 case $event instanceof Event\ProductGroupWasUpdated:
31 /** @var Order $order */
32 if ($order = $this->entityManager->find(Order::class, $event->aggregateId())) {
33 $order->setProductGroupId((string) $event->productGroup());
34 $order->setProductionMethod((string) $event->productionMethod());
35
36 foreach ($event->initialOrderProperties()->toArray() as $name => $value) {
37 if ($order->getOrderProperties()->containsKey($name)) { // existing property, we'll update it
38 /** @var OrderProperty $property */
39 $property = $order->getOrderProperties()->get($name);
40
41 if (null !== $value) {
42 $property->setValue($value);
43
44 $this->entityManager->persist($property);
45 } else {
46 $order->removeOrderProperty($property);
47 $this->entityManager->remove($property);
48 }
49 } else {
50 OrderProperty::createAndAddToOrder($order, $name, $value);
51 }
52 }
53
54 $this->entityManager->persist($order);
55 }
56
57 break;
58 case $event instanceof Event\OrderProductGroupOptionsWhereUpdated:
59 /** @var Order $order */
60 if ($order = $this->entityManager->find(Order::class, $event->orderId())) {
61 foreach ($event->properties()->toArray() as $name => $value) {
62 if ($order->getOrderProperties()->containsKey($name)) { // existing property, we'll update it
63 /** @var OrderProperty $property */
64 $property = $order->getOrderProperties()->get($name);
65
66 if (null !== $value) {
67 $property->setValue($value);
68
69 $this->entityManager->persist($property);
70 } else {
71 $order->removeOrderProperty($property);
72 $this->entityManager->remove($property);
73 }
74 } else {
75 $property = OrderProperty::createAndAddToOrder($order, $name, $value);
76
77 $order->addOrderProperty($property);
78 }
79 }
80
81 $this->entityManager->persist($order);
82 }
83
84 break;
85
86```
87
88To be complete I use this to create a new property;
89
90```php
91 public static function createAndAddToOrder(Order $order, string $name, $value)
92 {
93 // cause of polymorhic properties
94 switch ($name) {
95 case OrderPropertiesPath::SANDALS_CUSTOM_MADE_MODEL_COMPOSITION:
96 $property = new SandalsCustomMadeModelComposition();
97 break;
98 default:
99 $property = new OrderProperty();
100 }
101
102 $property
103 ->setPropertyId((string) Uuid::uuid5($order->getOrderId(), $name))
104 ->setName($name)
105 ->setValue($value);
106
107 $order->addOrderProperty($property);
108
109 return $property;
110 }
111```
112
113Somehow `$order->getOrderProperties()` seems to not return the correct list of properties. Perhaps from before the transaction started.
114
115Inspecting the property table I find that an order property exists *before* the transaction is started.
116
117```sql
118SELECT x.* FROM plhw_application_api_development.read_dossier_order_property x
119WHERE property_id='24a4e8ed-de31-5574-bed9-2da25ff29070'
120
121property_id |property |order_id |name |value|
122------------------------------------|--------------|------------------------------------|--------------|-----|
12324a4e8ed-de31-5574-bed9-2da25ff29070|order.property|b999faa0-2eac-4355-ad6a-4b1b2ab1345c|options.remark|"" |
124```
125
126## Performance difference
127
128```
129OPTION_PERSIST_BLOCK_SIZE = 1 => 100%
130OPTION_PERSIST_BLOCK_SIZE = 1000 => 16%
131```
132
133So would like to keep using 1000...