Skip to content

Commit

Permalink
Enclose naked add calls within UoW
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
subhashb committed Jun 12, 2024
1 parent b8a8dd7 commit 570ab90
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 1 deletion.
14 changes: 14 additions & 0 deletions src/protean/core/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/event_handler/test_uow_around_event_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
25 changes: 25 additions & 0 deletions tests/repository/test_add_uow.py
Original file line number Diff line number Diff line change
@@ -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(),
]
)

0 comments on commit 570ab90

Please sign in to comment.