From 570ab90846bce28ae8178caad7c1d3decec37767 Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Wed, 12 Jun 2024 14:17:43 -0700 Subject: [PATCH] Enclose naked `add` calls within UoW Ordinarialy, `add` method is called from within Command or Event handler methods. When `add` is invoked by itself, like in independent scripts, we need to ensure all ops in the `add` method are enclosed within a UoW. --- src/protean/core/repository.py | 14 +++++++++++ .../test_uow_around_event_handlers.py | 2 +- tests/repository/test_add_uow.py | 25 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 tests/repository/test_add_uow.py diff --git a/src/protean/core/repository.py b/src/protean/core/repository.py index 90c5b742..1219a585 100644 --- a/src/protean/core/repository.py +++ b/src/protean/core/repository.py @@ -6,8 +6,10 @@ from protean.container import Element, OptionsMixin from protean.core.aggregate import BaseAggregate +from protean.core.unit_of_work import UnitOfWork from protean.exceptions import IncorrectUsageError, NotSupportedError from protean.fields import HasMany, HasOne +from protean.globals import current_uow from protean.port.dao import BaseDAO from protean.port.provider import BaseProvider from protean.reflection import association_fields, has_association_fields @@ -112,6 +114,14 @@ def add(self, aggregate: BaseAggregate) -> BaseAggregate: # noqa: C901 transaction in progress, changes are committed immediately to the persistence store. This mechanism is part of the DAO's design, and is automatically used wherever one tries to persist data. """ + # `add` is typically invoked in handler methods in Command Handlers and Event Handlers, which are + # enclosed in a UoW automatically. Therefore, if there is a UoW in progress, we can assume + # that it is the active session. If not, we will start a new UoW and commit it after the operation + # is complete. + own_current_uow = None + if not (current_uow and current_uow.in_progress): + own_current_uow = UnitOfWork() + own_current_uow.start() # If there are HasMany/HasOne fields in the aggregate, sync child objects added/removed, if has_association_fields(aggregate): @@ -123,6 +133,10 @@ def add(self, aggregate: BaseAggregate) -> BaseAggregate: # noqa: C901 ): self._dao.save(aggregate) + # If we started a UnitOfWork, commit it now + if own_current_uow: + own_current_uow.commit() + return aggregate def _sync_children(self, entity): diff --git a/tests/event_handler/test_uow_around_event_handlers.py b/tests/event_handler/test_uow_around_event_handlers.py index 4b5a1769..5ec2c0e7 100644 --- a/tests/event_handler/test_uow_around_event_handlers.py +++ b/tests/event_handler/test_uow_around_event_handlers.py @@ -27,7 +27,7 @@ def send_email_notification(self, event: Registered) -> None: @mock.patch("protean.utils.mixins.UnitOfWork.__enter__") @mock.patch("tests.event_handler.test_uow_around_event_handlers.dummy") @mock.patch("protean.utils.mixins.UnitOfWork.__exit__") -def test_that_method_is_enclosed_in_uow(mock_exit, mock_dummy, mock_enter, test_domain): +def test_that_method_is_enclosed_in_uow(mock_exit, mock_dummy, mock_enter): mock_parent = mock.Mock() mock_parent.attach_mock(mock_enter, "m1") diff --git a/tests/repository/test_add_uow.py b/tests/repository/test_add_uow.py new file mode 100644 index 00000000..012abaa2 --- /dev/null +++ b/tests/repository/test_add_uow.py @@ -0,0 +1,25 @@ +import mock + +from .elements import Person + + +@mock.patch("protean.core.repository.UnitOfWork.start") +@mock.patch("protean.core.repository.UnitOfWork.commit") +def test_that_method_is_enclosed_in_uow(mock_commit, mock_start, test_domain): + mock_parent = mock.Mock() + + mock_parent.attach_mock(mock_start, "m1") + mock_parent.attach_mock(mock_commit, "m2") + + test_domain.register(Person) + test_domain.init(traverse=False) + with test_domain.domain_context(): + person = Person(first_name="John", last_name="Doe", age=29) + test_domain.repository_for(Person).add(person) + + mock_parent.assert_has_calls( + [ + mock.call.m1(), + mock.call.m2(), + ] + )