Practical PHP Patterns: Coarse Grained Lock
Join the DZone community and get the full member experience.
Join For FreeThe Optimistic and Pessimistic locks are basic patterns, which can in turn be composed with other ones. The Coarse Grained Lock pattern is a combination of the Aggregate pattern and of a lock-related one. The Coarse Grained Lock can be used without implementing Aggregate in an object model, but it works very well in conjunction with it, since it is a very efficient way of defining what are the subsets of the graph to lock at the same time.
The base assumption of this pattern is that there is a mismatch between the single objects of the application-wide graph and the actual Aggregate you're going to base your transactions on. Small classes and objects are ideal for domain modelling, while the necessity to lock every single object involved would put an unspoken constraint over the freedom of modelling to keep the system usable. It is not always practical to apply a lock pattern on single objects, thus this pattern solves the issue by defining a lock (optimistic or pessimistic) over a whole Aggregate.
An Aggregate refresh
The definition of an Aggregate is that of a subgraph used for breaking dependencies between the various objects. Defining Aggregates aids the graph in becoming loosely connected: the subgraph is the Aggregate, while the Facade object which encapsulates its internals is called the Aggregate Root.
The technical rule for implementing Aggregates is that no persistent references can be allowed (the ones save in the database, with foreign keys or similar mechanisms) to objects that are inside an aggregate. Only references to the root are permitted, while transient references can be obtained for the internal objects. It make sense to put in an Aggregate objects which are frequently used together and validated together.
For example, a class User which composes two Phonenumber and Address classes is probably the ideal target for constituting an Aggregate. In this example, I can call getAddresses() to obtain the Address objects and print them, but this reference is transient (destroyed at the end of the script): according to the domain model, I can't save it a a field of some other domain object. The User aggregate root should manage the lifecycle of all the internal objects.
As a side note, remember that nothing implies that transient references should be handed out by their Aggregate Roots: it would be ideal for them to be totally encapsulated and never exit the Aggregate's code, in order for the internals to change without the Aggregate's client code noticing.
Implementation
When an Aggregate is needed in a business transaction (as a whole) you lock the entire Aggregate, since we can assume that if the cohesion of the Aggregate is ideal, a Coarse Grained Lock does not result in unnecessary contention of the other near objects. Continuing with our example, it would make little sense for some User to edit his Address list while an administrator changes the User's Phonenumber.
Usually, when an optimistic lock is the chosen lock pattern, all the objects in the Aggregate share the same version of the root; the root is associated with the lock, and hides the various composed objects as well as the version field. When a change is applied to even only one of the internal objects, the version number is incremented, as if the Aggregate were a single, large object.
An alternative in this implementation is a shared lock, when the group of objects is not strictly an Aggregate and they share a common version object. However Aggregate boundaries and locked groups often coincide, thus it is reasonable to take advantage of the common ground between them.
The Pessimistic Lock version of this pattern is similar, since it focuses only on root objects as well.
These patterns are usually implemented while using an Object-Relational Mapper, there are some infrastructure choices to make along the way. For example the storage strategy for an ORM can be:
- in case of a shared lock, a version table, which is joined with the other objects' ones via a classic foreign key;
- in case of a root lock, a version column on the root object's table.
As always, the business domain and its requirements govern the definition of both Aggregates and locking groups.
The process of using a Coarse Grained Locks is similar to the following:
- the entire Aggregate is loaded, with its version field (assuming a root lock). Its data is modified by the user.
- When the entire Aggregate is saved, its version field has to be compared with the old one.
- If the transaction is successful, and there are changes to any object, the version number is incremented.
Advantages
This pattern simplifies the locking process, since there are actually less objects to lock, and less version fields to clutter the various tables.
Disadvantages
The pattern should be used when it makes sense in the domain model. Create fictional relationships between objects to model them as Aggregates or to take advantage of a Coarse Grained Lock is not proficient in the long run.
Example
We'll see a code sample that implements the pattern over an example Product-ShippingDetails annotated for taking advantage of Doctrine 2 features. Product is the Aggregate Root and the starting code has been taken from the reference manual.
The version field is kept on the root, but an issue in implementing this pattern is that Doctrine does not detect changes on child objects. This mechanism has to be accounted for in userland code.
The hack used here is to add another version field, which is incremented when the Aggregate internal items are modified. This way, a change is detected in the root entity, and the real optimistic lock version field can be checked for consistency and incremented as well.
<?php
/** @Entity */
class Product
{
/**
* @Id @Column(type="integer")
* @GeneratedValue
*/
private $id;
/**
* @Column(type="integer")
* @Version
*/
private $version;
/**
* @Column(type="integer")
* @Version
*/
private $aggregateVersion;
/**
* @OneToOne(targetEntity="ShippingDetails")
* @JoinColumn(name="shipping_id", referencedColumnName="id")
*/
private $shipping;
public function setShipping(ShippingDetails $shipping)
{
$this->shipping = $shipping;
$this->aggregateVersion++;
}
}
/** @Entity */
class ShippingDetails
{
public function __construct($description)
{
$this->_description = $description;
}
public function __toString()
{
return $this->_description;
}
}
Feel free to add anything to the example, if you feel there is a cleaner way to implement Optimistic Lock at the Aggregate level with minimal changes to ordinary Optimistic Lock code.
Opinions expressed by DZone contributors are their own.
Comments