From 48bc8108c8971042ae7416d7d8b468110993874b Mon Sep 17 00:00:00 2001 From: prolic Date: Tue, 8 May 2018 21:59:41 +0800 Subject: [PATCH] add option to disable identity map --- docs/interop_factories.md | 19 ++++++++++++ docs/repositories.md | 8 +++-- src/Aggregate/AggregateRepository.php | 21 +++++++++---- .../Aggregate/AggregateRepositoryFactory.php | 18 +++++++---- tests/Aggregate/AggregateRepositoryTest.php | 30 +++++++++++++++++++ 5 files changed, 82 insertions(+), 14 deletions(-) diff --git a/docs/interop_factories.md b/docs/interop_factories.md index 989bfc0..3bef17c 100644 --- a/docs/interop_factories.md +++ b/docs/interop_factories.md @@ -114,6 +114,25 @@ You can add your custom event store too (default is `EventStore::class`): ] ``` +You can also disable the identity map +```php +[ + 'prooph' => [ + 'event_sourcing' => [ + 'aggregate_repository' => [ + 'user_repository' => [ + 'repository_class' => MyUserRepository::class, + 'event_store' => MyCustomEventStore::class, // <-- Custom event store service id + 'aggregate_type' => MyUser::class, + 'aggregate_translator' => 'user_translator', + 'disable_identity_map' => true, + ], + ], + ], + ], +] +``` + Last but not least you can enable the so called "One-Stream-Per-Aggregate-Mode": ```php [ diff --git a/docs/repositories.md b/docs/repositories.md index 9561354..c4d468e 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -153,6 +153,8 @@ processes dealing with the same aggregate would run into concurrency issues very The test case has some more tests including snapshot usage and working with different stream names / strategies. Just browse through the test methods for details. +You can also disable the identity map by passing that option to the constructor (provided interop-factory can do this for you as well). + ## Aggregate Type Mapping It's possible to map an aggregate type `user` to an aggregate root class like `My\Model\User`. To do that, add the @@ -186,9 +188,9 @@ Example configuration: ## Loading of thousands aggregates If you need to load thousands of aggregates for reading only, your memory can be exhausted, because the -`AggregateRepository` uses an identity map. So every loaded aggregate is stored there, unless a commit is executed. If -you have a read only process, you should consider to clear the identity map at some time. This can be done by calling -`clearIdentityMap()`. +`AggregateRepository` uses an identity map (if it's not disabled). So every loaded aggregate is stored there, +unless a commit is executed. If you have a read only process, you should consider to clear the identity map +at some time. This can be done by calling `clearIdentityMap()`. ```php $thousandsOfAggregateIds = [ // lots of ids here ]; diff --git a/src/Aggregate/AggregateRepository.php b/src/Aggregate/AggregateRepository.php index 929820a..9f5b6b9 100644 --- a/src/Aggregate/AggregateRepository.php +++ b/src/Aggregate/AggregateRepository.php @@ -59,13 +59,19 @@ class AggregateRepository */ protected $oneStreamPerAggregate; + /** + * @var bool + */ + protected $disableIdentityMap; + public function __construct( EventStore $eventStore, AggregateType $aggregateType, AggregateTranslator $aggregateTranslator, SnapshotStore $snapshotStore = null, StreamName $streamName = null, - bool $oneStreamPerAggregate = false + bool $oneStreamPerAggregate = false, + bool $disableIdentityMap = false ) { $this->eventStore = $eventStore; $this->aggregateType = $aggregateType; @@ -73,6 +79,7 @@ public function __construct( $this->snapshotStore = $snapshotStore; $this->streamName = $streamName; $this->oneStreamPerAggregate = $oneStreamPerAggregate; + $this->disableIdentityMap = $disableIdentityMap; } /** @@ -114,7 +121,7 @@ public function saveAggregateRoot($eventSourcedAggregateRoot): void $this->eventStore->appendTo($streamName, new ArrayIterator($enrichedEvents)); } - if (isset($this->identityMap[$aggregateId])) { + if (! $this->disableIdentityMap && isset($this->identityMap[$aggregateId])) { unset($this->identityMap[$aggregateId]); } } @@ -126,14 +133,14 @@ public function saveAggregateRoot($eventSourcedAggregateRoot): void */ public function getAggregateRoot(string $aggregateId) { - if (isset($this->identityMap[$aggregateId])) { + if (! $this->disableIdentityMap && isset($this->identityMap[$aggregateId])) { return $this->identityMap[$aggregateId]; } if ($this->snapshotStore) { $eventSourcedAggregateRoot = $this->loadFromSnapshotStore($aggregateId); - if ($eventSourcedAggregateRoot) { + if ($eventSourcedAggregateRoot && ! $this->disableIdentityMap) { //Cache aggregate root in the identity map $this->identityMap[$aggregateId] = $eventSourcedAggregateRoot; } @@ -178,8 +185,10 @@ public function getAggregateRoot(string $aggregateId) $streamEvents ); - //Cache aggregate root in the identity map but without pending events - $this->identityMap[$aggregateId] = $eventSourcedAggregateRoot; + if (! $this->disableIdentityMap) { + //Cache aggregate root in the identity map but without pending events + $this->identityMap[$aggregateId] = $eventSourcedAggregateRoot; + } return $eventSourcedAggregateRoot; } diff --git a/src/Container/Aggregate/AggregateRepositoryFactory.php b/src/Container/Aggregate/AggregateRepositoryFactory.php index 378ab00..23088be 100644 --- a/src/Container/Aggregate/AggregateRepositoryFactory.php +++ b/src/Container/Aggregate/AggregateRepositoryFactory.php @@ -75,11 +75,18 @@ public function __invoke(ContainerInterface $container): AggregateRepository $repositoryClass = $config['repository_class']; if (! class_exists($repositoryClass)) { - throw ConfigurationException::configurationError(sprintf('Repository class %s cannot be found', $repositoryClass)); + throw ConfigurationException::configurationError(sprintf( + 'Repository class %s cannot be found', + $repositoryClass + )); } if (! is_subclass_of($repositoryClass, AggregateRepository::class)) { - throw ConfigurationException::configurationError(sprintf('Repository class %s must be a sub class of %s', $repositoryClass, AggregateRepository::class)); + throw ConfigurationException::configurationError(sprintf( + 'Repository class %s must be a sub class of %s', + $repositoryClass, + AggregateRepository::class + )); } $eventStore = $container->get($config['event_store']); @@ -96,15 +103,14 @@ public function __invoke(ContainerInterface $container): AggregateRepository $streamName = isset($config['stream_name']) ? new StreamName($config['stream_name']) : null; - $oneStreamPerAggregate = (bool) ($config['one_stream_per_aggregate'] ?? false); - return new $repositoryClass( $eventStore, $aggregateType, $aggregateTranslator, $snapshotStore, $streamName, - $oneStreamPerAggregate + $config['one_stream_per_aggregate'], + $config['disable_identity_map'] ); } @@ -131,6 +137,8 @@ public function defaultOptions(): iterable { return [ 'event_store' => EventStore::class, + 'one_stream_per_aggregate' => false, + 'disable_identity_map' => false, ]; } } diff --git a/tests/Aggregate/AggregateRepositoryTest.php b/tests/Aggregate/AggregateRepositoryTest.php index b463742..c7176d9 100644 --- a/tests/Aggregate/AggregateRepositoryTest.php +++ b/tests/Aggregate/AggregateRepositoryTest.php @@ -79,6 +79,36 @@ public function it_adds_a_new_aggregate(): void $this->assertEquals('John Doe', $fetchedUser->name()); } + /** + * @test + */ + public function it_ignores_identity_map_if_disabled(): void + { + $this->repository = new AggregateRepository( + $this->eventStore, + AggregateType::fromAggregateRootClass(User::class), + new AggregateTranslator(), + null, + null, + false, + true + ); + + $user = User::nameNew('John Doe'); + + $this->repository->saveAggregateRoot($user); + + $fetchedUser = $this->repository->getAggregateRoot( + $user->id() + ); + + $fetchedUser2 = $this->repository->getAggregateRoot( + $user->id() + ); + + $this->assertNotSame($fetchedUser, $fetchedUser2); + } + /** * @test */