-
-
Notifications
You must be signed in to change notification settings - Fork 130
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added example "content management system".
- Loading branch information
1 parent
befbc46
commit 7d61ae2
Showing
21 changed files
with
716 additions
and
224 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
System 1 - Content management system | ||
==================================== | ||
|
||
In this example, event notifications from the ``ContentManagementApplication`` from | ||
:doc:`/topics/examples/content-management` are processed and projected into an | ||
eventually-consistent full text search index, a searchable "materialized view" of | ||
the pages' body text just like :doc:`/topics/examples/searchable-content`. | ||
|
||
This is an example of CQRS. By separating the search engine "read model" from the content management | ||
"write model", the commands that update pages will perform faster. But, more importantly, the search | ||
engine can be redesigned and rebuilt by reprocessing those events. The projected searchable content | ||
can be deleted and rebuilt, perhaps also to include page titles, or timestamps, or other information | ||
contained in the domain events such as the authors, because it is updated by processing events. | ||
This is the main advantage of "CQRS" over the "inline" technique used in :doc:`/topics/examples/searchable-content` | ||
where the search index is simply updated whenever new events are recorded. Please note, it is possible | ||
to migrate from the "inline" technique to CQRS, by adding the downstream processing and then removing | ||
the inline updating, since the domain model is already event sourced. Similarly, other projections | ||
can be added to work alongside and concurrently with the updating of the search engine. | ||
|
||
Application | ||
----------- | ||
|
||
The ``SearchIndexApplication`` defined below is a :class:`~eventsourcing.system.ProcessApplication`. | ||
Its ``policy()`` function is coded to process the ``Page.Created`` and ``Page.BodyUpdated`` domain | ||
events of the ``ContentManagementApplication``. It also has a ``search()`` method that returns | ||
a list of page IDs. | ||
|
||
It that works in a similar way to the ``SearchableContentApplication`` class in | ||
:doc:`/topics/examples/searchable-content`, by setting variable keyword arguments | ||
``insert_pages`` and ``update_pages``. However, rather than populating variable | ||
keyword arguments in the ``save()`` method, it populates ``insert_pages`` and ``update_pages`` | ||
within its ``policy()`` function. The ``insert_pages`` and ``update_pages`` arguments are set | ||
on the :class:`~eventsourcing.application.ProcessingEvent` object passed into the ``policy()`` | ||
function, which carries an event notification ID that indicates the position | ||
in the application sequence of the domain event that is being processed. | ||
|
||
The application will be configured to run with a custom :class:`~eventsourcing.persistence.ProcessRecorder` | ||
so that search index records will be updated atomically with the inserting of a tracking record which | ||
indicates which upstream event notification has been processed. | ||
|
||
Because the ``Page.BodyUpdated`` event carries only the ``diff`` of the page body, the | ||
``policy()`` function must first select the current page body from its own records | ||
and then apply the diff as a patch. The "exactly once" semantics provided by the library's | ||
system module guarantees that the diffs will always be applied in the correct order. Without | ||
this guarantee, the projection could become inconsistent, with the consequence that the diffs | ||
will fail to be applied. | ||
|
||
.. literalinclude:: ../../../eventsourcing/examples/contentmanagementsystem/application.py | ||
|
||
System | ||
------ | ||
|
||
A :class:`~eventsourcing.system.System` of applications is defined, in which the | ||
``SearchIndexApplication`` follows the ``ContentManagementApplication``. This system | ||
can then be used in any :class:`~eventsourcing.system.Runner`. | ||
|
||
.. literalinclude:: ../../../eventsourcing/examples/contentmanagementsystem/system.py | ||
|
||
PostgreSQL | ||
---------- | ||
|
||
The ``PostgresSearchableContentRecorder`` from :doc:`/topics/examples/searchable-content` | ||
is used to define a custom :class:`~eventsourcing.persistence.ProcessRecorder` for PostgreSQL. | ||
The PostgreSQL :class:`~eventsourcing.postgres.Factory` class is extended to involve this custom recorder | ||
in a custom persistence module so that it can be used by the ``SearchIndexApplication``. | ||
|
||
|
||
.. literalinclude:: ../../../eventsourcing/examples/contentmanagementsystem/postgres.py | ||
|
||
SQLite | ||
------ | ||
|
||
The ``SqliteSearchableContentRecorder`` from :doc:`/topics/examples/searchable-content` | ||
is used to define a custom :class:`~eventsourcing.persistence.ProcessRecorder` for SQLite. | ||
The SQLite :class:`~eventsourcing.sqlite.Factory` class is extended to involve this custom recorder | ||
in a custom persistence module so that it can be used by the ``SearchIndexApplication``. | ||
|
||
.. literalinclude:: ../../../eventsourcing/examples/contentmanagementsystem/sqlite.py | ||
|
||
|
||
Test case | ||
--------- | ||
|
||
The test case ``ContentManagementSystemTestCase`` creates three pages, for 'animals', 'plants' | ||
and 'minerals'. Content is added to the pages. The content is searched with various queries and | ||
the search results are checked. The test is executed twice, once with the application configured | ||
for both PostgreSQL, and once for SQLite. | ||
|
||
.. literalinclude:: ../../../eventsourcing/examples/contentmanagementsystem/test_system.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
49 changes: 49 additions & 0 deletions
49
eventsourcing/examples/contentmanagementsystem/application.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
from typing import List, cast | ||
from uuid import UUID | ||
|
||
from eventsourcing.application import ProcessingEvent | ||
from eventsourcing.domain import DomainEventProtocol | ||
from eventsourcing.examples.contentmanagement.domainmodel import Page | ||
from eventsourcing.examples.contentmanagement.utils import apply_patch | ||
from eventsourcing.examples.searchablecontent.persistence import ( | ||
SearchableContentRecorder, | ||
) | ||
from eventsourcing.system import ProcessApplication | ||
|
||
|
||
class SearchIndexApplication(ProcessApplication): | ||
env = { | ||
"COMPRESSOR_TOPIC": "gzip", | ||
} | ||
|
||
def policy( | ||
self, | ||
domain_event: DomainEventProtocol, | ||
processing_event: ProcessingEvent, | ||
) -> None: | ||
if isinstance(domain_event, Page.Created): | ||
processing_event.saved_kwargs["insert_pages"] = [ | ||
( | ||
domain_event.originator_id, | ||
domain_event.slug, | ||
domain_event.title, | ||
domain_event.body, | ||
) | ||
] | ||
elif isinstance(domain_event, Page.BodyUpdated): | ||
recorder = cast(SearchableContentRecorder, self.recorder) | ||
page_id = domain_event.originator_id | ||
page_slug, page_title, page_body = recorder.select_page(page_id) | ||
page_body = apply_patch(page_body, domain_event.diff) | ||
processing_event.saved_kwargs["update_pages"] = [ | ||
( | ||
page_id, | ||
page_slug, | ||
page_title, | ||
page_body, | ||
) | ||
] | ||
|
||
def search(self, query: str) -> List[UUID]: | ||
recorder = cast(SearchableContentRecorder, self.recorder) | ||
return recorder.search_pages(query) |
Oops, something went wrong.