Optimistic Concurrency Control

There's another alternative: not using locks at all. Consider adding a version attribute to our Aggregates. When we persist them, the persistence engine sets 1 as the version of the persisted Aggregate. Later, we retrieve the same Aggregate and perform some changes to it. We persist the Aggregate. The persistence engine checks that the version we have is the same as the one that's currently persisted, version 1. The persistence engine persists the Aggregate with the new state and updates its version to 2. If multiple requests retrieve the same Aggregate, make some changes to it, and then try to persist it, the first request will work, and the second will experiment and error. The last request just changed an outdated version, so the persistence engine throws an error. However, the second request can try to retrieve the Aggregate again, merge the new status, attempt to perform the changes, and then persist the Aggregate.

According to Elasticsearch: The Definitive Guide:

This approach assumes that conflicts are unlikely to happen and does not block operations from being attempted. However, if the underlying data has been modified between reading and writing, the update will fail. It is then up to the application to decide how it should resolve the conflict. For instance, it could reattempt the update, using the fresh data, or it could report the situation to the user.

This idea was covered before, but it bears repeating. If you try to apply Optimistic Concurrency to this scenario where we're checking maximum wishes in the Application Service, it's not going to work. Why? We're making a new wish, so two requests would create two different wishes. How can we make it work? Well, we need an object to centralize adding the wishes. We could apply the Optimistic Concurrency trick on that object, so it looks like we need a parent object that will hold wishes. Any ideas?

To summarize, after reviewing concurrency controls, there's a pessimistic option working, but there are some concerns about performance impact. There's an optimistic option, but we need to find a parent object. Let's consider the final MakeWishService, but with some modifications:

class WishAggregateService
{
protected $userRepository;

public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}

protected function findUserOrFail($userId)
{
$user = $this->userRepository->ofId(new UserId($userId));
if (null === $user) {
throw new UserDoesNotExistException();
}

return $user;
}
}

class MakeWishService extends WishAggregateService
{
public function execute(MakeWishRequest $request)
{
$userId = $request->userId();
$address = $request->address();
$content = $request->content();

$user = $this->findUserOrFail($userId);

$user->makeWish($address, $content);

// Uncomment if your ORM can not flush
// the changes at the end of the request
// $this->userRepository->add($user);
}
}

We don't pass the WishId because it should be something internal to the User. makeWish doesn't return a Wish either; it stores the new wish internally. After the execution of the Application Service, our ORM will flush the changes performed on the $user to the database. Depending on how good our ORM is, we may need to explicitly add our User Entity again using the Repository. What changes to the User class are needed? First of all, there should be a collection that could hold all the wishes inside a user:

class User
{
// ...

/**
* @var ArrayCollection
*/
protected $wishes;

public function __construct(UserId $userId, $email, $password)
{
// ...
$this->wishes = new ArrayCollection();
// ...
}

// ...
}

The wishes property must be initialized in the User constructor. We could use a plain PHP array, but we've chosen to use an ArrayCollection. ArrayCollection is a PHP array with some extra features provided by the Doctrine Common Library, and it can be used separate from the ORM. We know that some of you may think that this could be a boundary leaking and that no references to any infrastructure should be here, but we really believe that's not the case. In fact, the same code works using plain PHP arrays. Let's see how the makeWish implementation is affected:

class User
{
// ...

/**
* @return void
*/
public function makeWish($address, $content)
{
if (count($this->wishes) >= 3) {
throw new MaxNumberOfWishesExceededException();
}

$this->wishes[] = new Wish(
new WishId,
$this->id(),
$address,
$content
);
}

// ...
}

So far, so good. Now, it's time to review how the rest of the operations are implemented.

Pushing for Eventual Consistency
It looks like the business doesn't want a user to have more than three wishes. That's going to force us to consider User as the root Aggregate with Wish inside. This will affect our design, performance, scalability issues, and so on. Consider what would happen if we could just let users add as many wishes as they wanted, beyond the limit. We could check who is exceeding that limit and let them know they need to purchase a premium account. Allowing a user to go over the limit and warning them by telephone afterward would be a really nice commercial strategy. That might even allow the developers on your team to avoid designing User and Wish as part of the same Aggregate, with User as its root. You've already seen the benefits of not designing a single Aggregate: maximum performance.
class UpdateWishService extends WishAggregateService
{
public function execute(UpdateWishRequest $request)
{
$userId = $request->userId();
$wishId = $request->wishId();
$email = $request->email();
$content = $request->content();

$user = $this->findUserOrFail($userId);

$user->updateWish(new WishId($wishId), $email, $content);
}
}

Because User and Wish now form an Aggregate, the Wish to be updated is no longer retrieved using the WishRepository. We fetch the user using the UserRepository. The operation of updating a Wish is performed via the root Entity, which is the User in this case. The WishId is necessary in order to identify which Wish we want to update:

class User
{
// ...

public function updateWish(WishId $wishId, $email, $content)
{
foreach ($this->wishes as $wish) {
if ($wish->id()->equals($wishId)) {
$wish->changeContent($content);
$wish->changeAddress($address);
break;
}
}
}
}

Depending on the features of your framework, this task may or may not be cheaper to perform. Iterating through all the wishes could mean making too many queries, or even worse, fetching too many rows, which will create a huge impact on memory. In fact, that's one of the main problems of having big Aggregates. So let's consider how to remove a Wish:

class RemoveWishService extends WishAggregateService
{
public function execute(RemoveWishRequest $request)
{
$userId = $request->userId();
$wishId = $request->wishId();

$user = $this->findUserOrFail($userId);

$user->removeWish($wishId):
}
}

As seen before, WishRepository is no longer necessary. We fetch the User using its Repository and perform the action of removing a Wish. In order to remove a Wish, we need to remove it from the inner collection. An option would be iterating through all the elements and matching the one with the same WishId:

class User
{
// ...

public function removeWish(WishId $wishId)
{
foreach ($this->wishes as $k => $wish) {
if ($wish->id()->equals($wishId)) {
unset($this->wishes[$k]);
break;
}
}
}

// ...
}

That's probably the most ORM-agnostic code possible. However, behind the scenes, Doctrine is fetching all the wishes and iterating through all of them. A more specific approach to fetch only the Entity needed that isn't so ORM agnostic would be the following:
Doctrine mapping must also be updated in order to make all the magic work as expected. While the Wish mapping remains the same, the User mapping has the new oneToMany unidirectional relationship:

LwDomainModelWishWish:
type: entity
table: lw_wish
repositoryClass:
LwInfrastructureDomainModelWishDoctrineWishRepository
id:
wishId:
column: id
type: WishId
fields:
address:
type: string
content:
type: text
userId:
type: UserId
column: user_id
LwDomainModelUserUser:
type: entity
id:
userId:
column: id
type: UserId
table: user
repositoryClass:
LwInfrastructureDomainModelUserDoctrineUserRepository
fields:
email:
type: string
password:
type: string
manyToMany:
wishes:
orphanRemoval: true
cascade: ["all"]
targetEntity: LwDomainModelWishWish
joinTable:
name: user_wishes
joinColumns:
user_id:
referencedColumnName: id
inverseJoinColumns:
wish_id:
referencedColumnName: id
unique: true

In the code above, there are two important configurations: orphanRemoval and cascade. According to the Doctrine 2 ORM Documentation on orphan removal and transitive persistence / cascade operations:

If an Entity of type A contains references to privately owned Entities B then if the reference from A to B is removed the entity B should also be removed, because it is not used anymore. OrphanRemoval works with one-to-one, one-to-many and many-to-many associations. When using the orphanRemoval=true option Doctrine makes the assumption that the entities are privately owned and will NOT be reused by other entities. If you neglect this assumption your entities will get deleted by Doctrine even if you assigned the orphaned entity to another one.

Persisting, removing, detaching, refreshing and merging individual entities can become pretty cumbersome, especially when a highly interweaved object graph is involved. Therefore Doctrine 2 provides a mechanism for transitive persistence through cascading of these operations. Each association to another entity or a collection of entities can be configured to automatically cascade certain operations. By default, no operations are cascaded.

For more information, please take a closer look at the Doctrine 2 ORM 2 Documentation on working with associations.

Finally, let's see how we can get the wishes from a user:

class ViewWishesService extends WishService
{
/**
* @return Wish[]
*/
public function execute(ViewWishesRequest $request)
{
return $this
->findUserOrFail($request->userId())
->wishes();
}
}

As mentioned before, especially in this scenario using Aggregates, returning a collection of Wishes is not the best solution. You should never return Domain Entities, as this will prevent code outside of your Application Services — such as Controllers or your UI — from unexpectedly modifying them. With Aggregates, it makes even more sense. Entities that aren't root — the ones that belong to the Aggregate but aren't root  — should appear private to others outside.

We'll go deeper into this in the Chapter 11, Application. For now, to summarize, you have different options:

  • The Application Service returns a DTO build accessing Aggregates information.
  • The Application Service returns a DTO returned by the Aggregate.
  • The Application Service uses an Output dependency where it writes the Aggregate. Such an Output dependency will handle the transformation to a DTO or other format.
Render the Number of Wishes  As an exercise, consider that we want to render the number of wishes a user has made on their account page. How would you implement this, considering User and Wish don't form an Aggregate? How would you implement it if User and Wish did form an Aggregate? Consider how Eventual Consistency could help in your solutions.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.186.92