Unit of Work¶
The frequent.unit_of_work module provides
base classes for an implementation of the unit of work pattern. This pattern
is (sometimes) used when working with object persistence/storage (e.g. ORMs).
The primary advantage of this pattern is the transactional nature of units
of work. Regardless of the storage back-end implemented with the
UnitOfWork
subclass (provided it can
support staging changes to be made, persisting those changes and deleting those
changes) all work performed inside a
UnitOfWork
context block is a
transaction.
Note
This pattern pairs well with the Repository pattern and a command-based Messaging Framework.
Usage¶
The abstract classes UnitOfWork
and
UnitOfWorkManager
can be
extended to for specific use-cases.
Creating a UnitOfWork Subclass¶
The UnitOfWork
class has two abstract
methods which need to be implemented in subclasses:
- commit()
Persists the changes made using the unit of work to the storage back-end.
- rollback()
Undo any changes made while using the unit of work and do not persist anything to the storage back-end.
A simple implementation might look like this:
from frequent.unit_of_work import UnitOfWork
class MyUnitOfWork(UnitOfWork):
def commit(self):
# Code to persist changes
return
def rollback(self):
# Code to dispose of changes
return
If there’s anything that needs to be done upon entering/exiting the
UnitOfWork
context, you can customize
two additional methods:
class MyUnitOfWork(UnitOfWork):
...
def __enter__(self):
# Context entry setup code here
return super().__enter__()
def __exit__(self, exc_type, exc_value, traceback):
# Pre commit/rollback teardown code here
super().__exit__(exc_type, exc_value, traceback)
# Post commit/rollback teardown code here
return
Warning
The order of the super()
calls in the __enter__
and __exit__
methods matters!
In the __enter__
call the superclass’s method returns self
(you
could just return self
if you wanted, though this approach ensures that
any general entry-code that may exist in future versions of the base class
will be executed).
In the __exit__
call the superclass’s method will call either
commit()
or rollback()
depending on the exit conditions. So the
location of that call matters and could vary depending on your particular
use case.
Creating a UnitOfWorkManager Subclass¶
The UnitOfWorkManager
class
has a single abstract method to implement in subclasses:
- start()
This returns a new
UnitOfWork
instance, ready to use.
Continuing from the example above the associated
UnitOfWorkManager
class would
look something like:
from frequent.unit_of_work import UnitOfWorkManager
class MyUnitOfWorkManager(UnitOfWorkManager):
def start(self):
return MyUnitOfWork()
The UnitOfWorkManager
class may
appear to be a useless abstraction from the above example (and in this case it
kind of is), but its usefulness can be seen in the (more realistic)
extended example given below.
Using the UnitOfWork and Manager¶
Now to use our new subclasses, remember the
commit
method will be called
upon exiting the context block (or the
rollback
call if something
went wrong). You’re free to call
commit
at any point within the
block to persist any changes up to that point (if it makes sense for your
use-case).
>>> uowm = MyUnitOfWorkManager()
>>> with uowm.start() as uow:
... # Code for doing work in this block
... uow.commit() # Persist changes up to this point (if you want/need to)
... # More work code
Important
You do not have to call the
commit
method at the end
of the with
statement block, it will automatically be called upon a
successful exit of the context.
Extended Example¶
Let’s suppose we’re using SQLAlchemy’s ORM for our storage back-end. Then our unit of work class would look something like this:
class MyUnitOfWork(UnitOfWork):
def __init__(self, sessionmaker) -> None:
self._sessionmaker = sessionmaker
self.session = None
return
def __enter__(self):
# Create a new session
self.session = self._sessionmaker()
return super().__enter__()
def __exit__(self, exc_type, exc_value, traceback):
super().__exit__(exc_type, exc_value, traceback)
# Be sure to close the session when done, regardless
self.session.close()
self.session = None
return
def commit(self):
self.session.commit()
return
def rollback(self):
self.session.rollback()
return
Now we’ll instantiate the
UnitOfWorkManager
class to
spin-up new MyUnitOfWork
instances to use:
class MyUnitOfWorkManager(UnitOfWorkManager):
def __init__(self, sessionmaker):
self._sessionmaker = sessionmaker
return
def start(self):
return MyUnitOfWork(self._sessionmaker)
Lastly, let’s wrap all our user management code inside another class whose sole
purpose is working with User
(and other associated) objects. We’ll want
the ability to create users from this user manager class, but let’s also
suppose that we also have a (separate) UserProfile
object which stores some
basic information and settings about our users. This object is always created
when we create our User
objects. This is where the advantage of the unit
of work pattern can really be seen:
class UserManager(object):
def __init__(self, uow_manager):
self.uowm = uow_manager
return
def create_user(self, name, email, location=None, receive_emails=True):
with self.uowm.start() as uow:
# Create and add the user
user = User(name)
uow.session.add(new_user)
uow.session.flush()
# Create and add the profile
profile = UserProfile(user.id, email, location, receive_emails)
uow.session.add(new_profile)
return new_user
The above code demonstrates the advantages (discussed earlier)
of this design pattern. In the above example if an error occurred at any point in creating
any new user (for example if we do some validation on the email
or the user
already exists), the uow
would have been rolled-back automatically and the
exception raised. If everything works as expected then the uow
will call
the commit()
method and both the new user and profile objects will be
persisted to our storage back-end upon exiting the with
context block.
Thus we have only two possible outcomes when adding User
objects:
Both the user and their profile are added to our system.
Neither the user nor their profile are added to our storage system.
The point is, we won’t wind up in some in-between state where the user is added but the associated profile is not (or vice-versa).
Useful Links¶
References¶
The unit of work pattern reference, originally from Martin Fowler’s classic book on enterprise software architecture patterns:
For an excellent overview and tutorial of this pattern in Python see this post (and the others in the series, as well as the examples in the github repository) from Bob Gregory:
Frequent API¶
- Module
- Abstract Classes