You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Chapter 6 Excercise for the Reader: Separate UoW and Context Manager #47
experiment with separating the UoW (whose responsibilities are
commit() , rollback() , and providing the .batches repository) from the
context manager, whose job is to initialize things, and then do the commit
or rollback on exit.
Could someone give a solution for this exercise?
Following is my code example. Would there be a point in splitting FakeUnitOfWork?
@chrisgrf, here's how I've implemented this pattern in a few projects at my work:
service_layer/ports.py:
importtypingastclassIUnitOfWorkContext(t.Protocol):
batches_repository: IBatchesRepositoryproducts_repository: IProductsRepositoryclassIUnitOfWork(t.ContextManager[IUnitOfWorkContext], t.Protocol):
defcommit(self) ->None:
""" Commit unit of work :raises UnitOfWorkNotStarted if called outside the context manager scope """
...
defrollback(self) ->None:
""" Rollback unit of work :raises UnitOfWorkNotStarted if called outside the context manager scope """
...
Note: the protocol conveniently defines the base type t.ContextManager[IUnitOfWorkContext] using ContextManager from the typing module. This defines the __enter__ and __exit__ methods for you, i.e. that __enter__ returns a IUnitOfWorkContext.
One of the reasons I implemented it like this was that it allows you to centralise the UoW responsibility within the message bus instead of in the command handler which allows you to execute multiple commands within the same UoW (bends the rules but this might be useful). So the definition of your command handlers becomes:
However, I've changed this a lot since I started using this pattern. Here are some thoughts/notes:
Recently, I've actually dropped the IUnitOfWorkContext object altogether in favour of injecting the repositories directly into the command handlers rather than accessing them through the UoW. The problem is the UoW/context tends to become a single god object that provides access to all adapters, not just the repositories. E.g. something like:
defsome_command_handler(
batches_repository: BatchesRepository,
products_repository: IProductsRepository,
product_id: int
):
product=product_repository.get_by_id(product_id)
...
However, this requires you re-think the relationship between the session, UoW and repositories since the repos still need access to the session. Instead, you must create the session in the bootstrap function and pass it into the UoW and repositories rather than the UoW being the sessionmaker.
Note: I've always found the relationship of the session and UoW to be a bit mysterious. I've come to the conclusion that they aren't the same thing, and it's OK to create a session to pass around to different adapters as required. There's an example in Vaughn Vernon's red book in Chapter 12: "Repositories", Section: "Considerations for a TopLink Implementation" that shows a Java ORM called TopLink that has a API where you get the UoW from the session, e.g.:
The naming of the two classes, IUnitOfWork and IUnitOfWorkContext, is a bit weird. You might want to name them something like, IUnitOfWorkManager and IUnitOfWork, respectively.
I've also changed around where the commit and rollback methods live. The benefit of putting them on (what I called) the IUnitOfWorkContext object is that it eliminates the UnitOfWorkNotStarted error state since you have to use the context manager to get the ctx back. This is I think what the exercise was getting at. However, I chose to keep them in IUnitOfWork mainly because the command handlers don't need those methods (my team wanted the UoW to commit by default and throw exceptions for errors - all of this is handled in the PostgresUnitOfWork as you can see.)
@gregbrowndev Thanks for a very detailed and interesting answer!
However, our task was to separate the UoW form the context manager. How good is your version, given that the __enter__ and __exit__ dunder methods are still inside UoW?
...
def allocate(
orderid: str,
sku: str,
qty: int,
uow_manager: unit_of_work.AbstractUnitOfWorkManager
) -> str:
line = OrderLine(orderid, sku, qty)
with uow_manager as uow:
batches = uow.batches.list()
if not is_valid_sku(line.sku, batches):
raise InvalidSku(f"Invalid sku {line.sku}")
batchref = model.allocate(line, batches)
uow.commit()
return batchref
...
In my opinion, this solution most explicitly shows that UoW only has the necessary responsibilities.
If I am wrong, I will be glad to hear comments and contrarguments!
Activity
gregbrowndev commentedon Jul 29, 2022
@chrisgrf, here's how I've implemented this pattern in a few projects at my work:
service_layer/ports.py:
Note: the protocol conveniently defines the base type
t.ContextManager[IUnitOfWorkContext]using ContextManager from the typing module. This defines the__enter__and__exit__methods for you, i.e. that__enter__returns aIUnitOfWorkContext.This is the concrete implementation:
It can be used like this:
One of the reasons I implemented it like this was that it allows you to centralise the UoW responsibility within the message bus instead of in the command handler which allows you to execute multiple commands within the same UoW (bends the rules but this might be useful). So the definition of your command handlers becomes:
However, I've changed this a lot since I started using this pattern. Here are some thoughts/notes:
Recently, I've actually dropped the
IUnitOfWorkContextobject altogether in favour of injecting the repositories directly into the command handlers rather than accessing them through the UoW. The problem is the UoW/context tends to become a single god object that provides access to all adapters, not just the repositories. E.g. something like:However, this requires you re-think the relationship between the
session,UoWand repositories since the repos still need access to thesession. Instead, you must create thesessionin thebootstrapfunction and pass it into theUoWand repositories rather than theUoWbeing thesessionmaker.Note: I've always found the relationship of the session and UoW to be a bit mysterious. I've come to the conclusion that they aren't the same thing, and it's OK to create a
sessionto pass around to different adapters as required. There's an example in Vaughn Vernon's red book in Chapter 12: "Repositories", Section: "Considerations for a TopLink Implementation" that shows a Java ORM called TopLink that has a API where you get the UoW from the session, e.g.:The naming of the two classes,
IUnitOfWorkandIUnitOfWorkContext, is a bit weird. You might want to name them something like,IUnitOfWorkManagerandIUnitOfWork, respectively.I've also changed around where the
commitandrollbackmethods live. The benefit of putting them on (what I called) theIUnitOfWorkContextobject is that it eliminates theUnitOfWorkNotStartederror state since you have to use the context manager to get thectxback. This is I think what the exercise was getting at. However, I chose to keep them inIUnitOfWorkmainly because the command handlers don't need those methods (my team wanted theUoWto commit by default and throw exceptions for errors - all of this is handled in thePostgresUnitOfWorkas you can see.)Hope that is helpful!
bl1nkker commentedon Jan 19, 2023
@gregbrowndev Thanks for a very detailed and interesting answer!
However, our task was to separate the UoW form the context manager. How good is your version, given that the
__enter__and__exit__dunder methods are still inside UoW?My solution looks like this (unit_of_work.py):
and its usage (services.py):
In my opinion, this solution most explicitly shows that UoW only has the necessary responsibilities.
If I am wrong, I will be glad to hear comments and contrarguments!