Skip to content

Latest commit

 

History

History
956 lines (755 loc) · 43.9 KB

File metadata and controls

956 lines (755 loc) · 43.9 KB

Unit of Work Pattern

工作单元模式

In this chapter we’ll introduce the final piece of the puzzle that ties together the Repository and Service Layer patterns: the Unit of Work pattern.

在本章中,我们将介绍拼接仓储模式和服务层模式的最后一块拼图:工作单元 模式。

If the Repository pattern is our abstraction over the idea of persistent storage, the Unit of Work (UoW) pattern is our abstraction over the idea of atomic operations. It will allow us to finally and fully decouple our service layer from the data layer.

如果说仓储模式是对持久化存储概念的抽象,那么工作单元(Unit of Work,UoW)模式就是对 原子操作 概念的抽象。 它将使我们最终完全将服务层与数据层解耦。

Without UoW: API talks directly to three layers(没有工作单元:API 直接与三层交互) shows that, currently, a lot of communication occurs across the layers of our infrastructure: the API talks directly to the database layer to start a session, it talks to the repository layer to initialize SQLAlchemyRepository, and it talks to the service layer to ask it to allocate.

Without UoW: API talks directly to three layers(没有工作单元:API 直接与三层交互) 展示了当前我们的基础设施各层之间存在大量通信:API 直接与数据库层交互以启动会话, 与仓储层交互以初始化 SQLAlchemyRepository,并与服务层交互以请求进行分配。

Tip

The code for this chapter is in the chapter_06_uow branch on GitHub:

本章的代码位于 chapter_06_uow 分支 GitHub

git clone https://github.com/cosmicpython/code.git
cd code
git checkout chapter_06_uow
# or to code along, checkout Chapter 4:
git checkout chapter_04_service_layer
apwp 0601
Figure 1. Without UoW: API talks directly to three layers(没有工作单元:API 直接与三层交互)

With UoW: UoW now manages database state(有了工作单元:UoW 现在管理数据库状态) shows our target state. The Flask API now does only two things: it initializes a unit of work, and it invokes a service. The service collaborates with the UoW (we like to think of the UoW as being part of the service layer), but neither the service function itself nor Flask now needs to talk directly to the database.

With UoW: UoW now manages database state(有了工作单元:UoW 现在管理数据库状态) 展示了我们的目标状态。现在,Flask API 仅执行两件事:初始化一个工作单元,并调用一个服务。 服务与工作单元协作(我们倾向于将工作单元视为服务层的一部分),但服务函数本身和 Flask 都不再需要直接与数据库交互。

And we’ll do it all using a lovely piece of Python syntax, a context manager.

我们将通过一段优雅的 Python 语法——上下文管理器来实现这一切。

apwp 0602
Figure 2. With UoW: UoW now manages database state(有了工作单元:UoW 现在管理数据库状态)

The Unit of Work Collaborates with the Repository

工作单元与仓储协作

Let’s see the unit of work (or UoW, which we pronounce "you-wow") in action. Here’s how the service layer will look when we’re finished:

让我们看看工作单元(Unit of Work,简称 UoW,我们发音为“you-wow”)的实际应用。当我们完成后,服务层将如下所示:

Example 1. Preview of unit of work in action (src/allocation/service_layer/services.py)(工作单元实际应用的预览)
def allocate(
    orderid: str, sku: str, qty: int,
    uow: unit_of_work.AbstractUnitOfWork,
) -> str:
    line = OrderLine(orderid, sku, qty)
    with uow:  #(1)
        batches = uow.batches.list()  #(2)
        ...
        batchref = model.allocate(line, batches)
        uow.commit()  #(3)
  1. We’ll start a UoW as a context manager. 我们将以上下文管理器的形式启动一个工作单元。

  2. uow.batches is the batches repo, so the UoW provides us access to our permanent storage. uow.batches 是批次仓储,因此,工作单元为我们提供了访问持久存储的途径。

  3. When we’re done, we commit or roll back our work, using the UoW. 当我们完成后,我们使用工作单元提交或回滚我们的工作。

The UoW acts as a single entrypoint to our persistent storage, and it keeps track of what objects were loaded and of the latest state.[1]

工作单元充当我们持久化存储的单一入口,并且它会追踪加载了哪些对象以及它们的最新状态。脚注: 你可能已经碰到过使用“协作者”一词来描述为了实现目标而协同工作的对象。在对象建模的意义上,工作单元和仓储就是协作者的一个很好的例子。 在责任驱动设计中,那些在各自职责中协作的对象簇被称为 对象邻域(object neighborhoods),从我们的专业角度来看,这个称呼简直可爱极了。

This gives us three useful things:

这为我们提供了三大好处:

  • A stable snapshot of the database to work with, so the objects we use aren’t changing halfway through an operation 一个数据库的稳定快照,供我们使用,这样我们操作过程中使用的对象就不会中途发生变化。

  • A way to persist all of our changes at once, so if something goes wrong, we don’t end up in an inconsistent state 一种一次性持久化所有更改的方法,这样如果出现问题,我们就不会陷入不一致的状态。

  • A simple API to our persistence concerns and a handy place to get a repository 一个简化的持久化操作接口,以及一个获取仓储的方便位置。

Test-Driving a UoW with Integration Tests

通过集成测试对工作单元进行测试驱动开发

Here are our integration tests for the UOW:

以下是我们针对工作单元的集成测试:

Example 2. A basic "round-trip" test for a UoW (tests/integration/test_uow.py)(针对工作单元的基础“往返”测试)
def test_uow_can_retrieve_a_batch_and_allocate_to_it(session_factory):
    session = session_factory()
    insert_batch(session, "batch1", "HIPSTER-WORKBENCH", 100, None)
    session.commit()

    uow = unit_of_work.SqlAlchemyUnitOfWork(session_factory)  #(1)
    with uow:
        batch = uow.batches.get(reference="batch1")  #(2)
        line = model.OrderLine("o1", "HIPSTER-WORKBENCH", 10)
        batch.allocate(line)
        uow.commit()  #(3)

    batchref = get_allocated_batch_ref(session, "o1", "HIPSTER-WORKBENCH")
    assert batchref == "batch1"
  1. We initialize the UoW by using our custom session factory and get back a uow object to use in our with block. 我们通过使用自定义的会话工厂初始化工作单元,并得到一个 uow 对象,以便在我们的 with 块中使用。

  2. The UoW gives us access to the batches repository via uow.batches. 工作单元通过 uow.batches 为我们提供访问批次仓储的途径。

  3. We call commit() on it when we’re done. 当我们完成后,我们调用 commit()

For the curious, the insert_batch and get_allocated_batch_ref helpers look like this:

对于感兴趣的读者,insert_batchget_allocated_batch_ref 辅助函数如下所示:

Example 3. Helpers for doing SQL stuff (tests/integration/test_uow.py)(用于处理 SQL 的辅助工具)
def insert_batch(session, ref, sku, qty, eta):
    session.execute(
        "INSERT INTO batches (reference, sku, _purchased_quantity, eta)"
        " VALUES (:ref, :sku, :qty, :eta)",
        dict(ref=ref, sku=sku, qty=qty, eta=eta),
    )


def get_allocated_batch_ref(session, orderid, sku):
    [[orderlineid]] = session.execute(  #(1)
        "SELECT id FROM order_lines WHERE orderid=:orderid AND sku=:sku",
        dict(orderid=orderid, sku=sku),
    )
    [[batchref]] = session.execute(  #(1)
        "SELECT b.reference FROM allocations JOIN batches AS b ON batch_id = b.id"
        " WHERE orderline_id=:orderlineid",
        dict(orderlineid=orderlineid),
    )
    return batchref
  1. The = syntax is a little too-clever-by-half, apologies. What’s happening is that session.execute returns a list of rows, where each row is a tuple of column values; in our specific case, it’s a list of one row, which is a tuple with one column value in. The double-square-bracket on the left hand side is doing (double) assignment-unpacking to get the single value back out of these two nested sequences. It becomes readable once you’ve used it a few times! = 语法或许显得有些过于巧妙,我们对此表示歉意。实际上,这里发生的事情是 session.execute 返回了一列行的列表, 其中每一行是一个包含列值的元组;在我们的具体场景中,这是一个只有一行的列表,而这行是一个仅包含一个列值的元组。 左侧的双重方括号完成了(双重)解包赋值,从这两个嵌套序列中提取出唯一的值。使用过几次后,这种写法就会变得清晰易读了!

Unit of Work and Its Context Manager

工作单元及其上下文管理器

In our tests we’ve implicitly defined an interface for what a UoW needs to do. Let’s make that explicit by using an abstract base class:

在我们的测试中,实际上已经隐式定义了工作单元需要实现的接口。现在,让我们通过使用抽象基类将其明确化:

Example 4. Abstract UoW context manager (src/allocation/service_layer/unit_of_work.py)(抽象工作单元上下文管理器)
class AbstractUnitOfWork(abc.ABC):
    batches: repository.AbstractRepository  #(1)

    def __exit__(self, *args):  #(2)
        self.rollback()  #(4)

    @abc.abstractmethod
    def commit(self):  #(3)
        raise NotImplementedError

    @abc.abstractmethod
    def rollback(self):  #(4)
        raise NotImplementedError
  1. The UoW provides an attribute called .batches, which will give us access to the batches repository. 工作单元提供了一个名为 .batches 的属性,它使我们能够访问批次仓储。

  2. If you’ve never seen a context manager, __enter__ and __exit__ are the two magic methods that execute when we enter the with block and when we exit it, respectively. They’re our setup and teardown phases. 如果你从未见过上下文管理器,__enter____exit__ 是两个魔法方法, 分别在我们进入 with 块和退出 with 块时执行。它们对应我们的设置(setup)和销毁(teardown)阶段。

  3. We’ll call this method to explicitly commit our work when we’re ready. 当我们准备好时,我们将调用此方法来显式提交我们的工作。

  4. If we don’t commit, or if we exit the context manager by raising an error, we do a rollback. (The rollback has no effect if commit() has been called. Read on for more discussion of this.) 如果我们没有调用 commit(),或者通过引发错误退出上下文管理器,我们将执行一次 rollback(回滚)。 (如果已经调用了 commit(),回滚将不起作用。后续会有更多相关讨论。)

The Real Unit of Work Uses SQLAlchemy Sessions

使用 SQLAlchemy 会话的真实工作单元

The main thing that our concrete implementation adds is the database session:

我们的具体实现主要增加了一个数据库会话:

Example 5. The real SQLAlchemy UoW (src/allocation/service_layer/unit_of_work.py)(真实的 SQLAlchemy 工作单元)
DEFAULT_SESSION_FACTORY = sessionmaker(  #(1)
    bind=create_engine(
        config.get_postgres_uri(),
    )
)


class SqlAlchemyUnitOfWork(AbstractUnitOfWork):
    def __init__(self, session_factory=DEFAULT_SESSION_FACTORY):
        self.session_factory = session_factory  #(1)

    def __enter__(self):
        self.session = self.session_factory()  # type: Session  #(2)
        self.batches = repository.SqlAlchemyRepository(self.session)  #(2)
        return super().__enter__()

    def __exit__(self, *args):
        super().__exit__(*args)
        self.session.close()  #(3)

    def commit(self):  #(4)
        self.session.commit()

    def rollback(self):  #(4)
        self.session.rollback()
  1. The module defines a default session factory that will connect to Postgres, but we allow that to be overridden in our integration tests so that we can use SQLite instead. 该模块定义了一个默认会话工厂,用于连接到 Postgres,但我们允许在集成测试中重写它,这样我们就可以改用 SQLite。

  2. The __enter__ method is responsible for starting a database session and instantiating a real repository that can use that session. __enter__ 方法负责启动一个数据库会话并实例化一个能够使用该会话的真实仓储。

  3. We close the session on exit. 在退出时,我们会关闭会话。

  4. Finally, we provide concrete commit() and rollback() methods that use our database session. 最后,我们提供了具体的 commit()rollback() 方法来操作我们的数据库会话。

Fake Unit of Work for Testing

用于测试的伪工作单元

Here’s how we use a fake UoW in our service-layer tests:

以下是我们在服务层测试中使用伪工作单元的方式:

Example 6. Fake UoW (tests/unit/test_services.py)(伪工作单元)
class FakeUnitOfWork(unit_of_work.AbstractUnitOfWork):
    def __init__(self):
        self.batches = FakeRepository([])  #(1)
        self.committed = False  #(2)

    def commit(self):
        self.committed = True  #(2)

    def rollback(self):
        pass


def test_add_batch():
    uow = FakeUnitOfWork()  #(3)
    services.add_batch("b1", "CRUNCHY-ARMCHAIR", 100, None, uow)  #(3)
    assert uow.batches.get("b1") is not None
    assert uow.committed


def test_allocate_returns_allocation():
    uow = FakeUnitOfWork()  #(3)
    services.add_batch("batch1", "COMPLICATED-LAMP", 100, None, uow)  #(3)
    result = services.allocate("o1", "COMPLICATED-LAMP", 10, uow)  #(3)
    assert result == "batch1"
...
  1. FakeUnitOfWork and FakeRepository are tightly coupled, just like the real UnitofWork and Repository classes. That’s fine because we recognize that the objects are collaborators. FakeUnitOfWorkFakeRepository 紧密耦合,就像真实的 UnitOfWorkRepository 类一样。 这没有问题,因为我们知道这些对象只是协作者。

  2. Notice the similarity with the fake commit() function from FakeSession (which we can now get rid of). But it’s a substantial improvement because we’re now faking out code that we wrote rather than third-party code. Some people say, "Don’t mock what you don’t own". 注意它与 FakeSession 中伪造的 commit() 函数的相似之处(我们现在可以将其移除)。但这是一项重要的改进, 因为我们现在是在 伪造 我们自己编写的代码,而不是第三方代码。 有些人会说, “不要模拟你不拥有的东西”

  3. In our tests, we can instantiate a UoW and pass it to our service layer, rather than passing a repository and a session. This is considerably less cumbersome. 在我们的测试中,我们可以实例化一个工作单元并将其传递给服务层,而不是传递一个仓储和一个会话。这要简单得多。

Don’t Mock What You Don’t Own(不要模拟你不拥有的东西)

Why do we feel more comfortable mocking the UoW than the session? Both of our fakes achieve the same thing: they give us a way to swap out our persistence layer so we can run tests in memory instead of needing to talk to a real database. The difference is in the resulting design.

为什么我们对模拟工作单元比模拟会话更感到放心? 我们的两个伪对象(Fake)实现了相同的目标:为我们提供一种替换持久化层的方式,这样我们可以在内存中运行测试, 而无需与真实数据库交互。区别在于它们带来了不同的设计结果。

If we cared only about writing tests that run quickly, we could create mocks that replace SQLAlchemy and use those throughout our codebase. The problem is that Session is a complex object that exposes lots of persistence-related functionality. It’s easy to use Session to make arbitrary queries against the database, but that quickly leads to data access code being sprinkled all over the codebase. To avoid that, we want to limit access to our persistence layer so each component has exactly what it needs and nothing more.

如果我们只关心编写运行速度快的测试,那么我们可以创建替代 SQLAlchemy 的模拟对象(mocks),并在整个代码库中使用它们。 问题在于,Session 是一个复杂的对象,它暴露了许多与持久化相关的功能。使用 Session 可以随意对数据库进行查询, 但这很容易导致数据访问代码散布在代码库的各个地方。为了避免这种情况,我们希望限制对持久化层的访问,以保证每个组件只拥有它需要的内容,不多也不少。

By coupling to the Session interface, you’re choosing to couple to all the complexity of SQLAlchemy. Instead, we want to choose a simpler abstraction and use that to clearly separate responsibilities. Our UoW is much simpler than a session, and we feel comfortable with the service layer being able to start and stop units of work.

通过耦合到 Session 接口,你实际上选择了与 SQLAlchemy 的所有复杂性进行耦合。而我们希望选择一个更简单的抽象,并以此清晰地分离职责。 我们的 UoW 比 Session 简单得多,我们也对服务层能够启动和停止工作单元感到放心。

"Don’t mock what you don’t own" is a rule of thumb that forces us to build these simple abstractions over messy subsystems. This has the same performance benefit as mocking the SQLAlchemy session but encourages us to think carefully about our designs.

“不要模拟你不拥有的东西”是一条经验法则,它促使我们在混乱的子系统之上构建这些简单的抽象。这不仅与模拟 SQLAlchemy 会话具有相同的性能优势, 还鼓励我们认真思考我们的设计。

Using the UoW in the Service Layer

在服务层中使用工作单元

Here’s what our new service layer looks like:

以下是新的服务层代码:

Example 7. Service layer using UoW (src/allocation/service_layer/services.py)(使用工作单元的服务层)
def add_batch(
    ref: str, sku: str, qty: int, eta: Optional[date],
    uow: unit_of_work.AbstractUnitOfWork,  #(1)
):
    with uow:
        uow.batches.add(model.Batch(ref, sku, qty, eta))
        uow.commit()


def allocate(
    orderid: str, sku: str, qty: int,
    uow: unit_of_work.AbstractUnitOfWork,  #(1)
) -> str:
    line = OrderLine(orderid, sku, qty)
    with 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
  1. Our service layer now has only the one dependency, once again on an abstract UoW. 我们的服务层现在只有一个依赖,再次依赖于一个 抽象的 工作单元。

Explicit Tests for Commit/Rollback Behavior

针对提交/回滚行为的明确测试

To convince ourselves that the commit/rollback behavior works, we wrote a couple of tests:

为让我们确信提交/回滚行为的正常运作,我们编写了几个测试:

Example 8. Integration tests for rollback behavior (tests/integration/test_uow.py)(针对回滚行为的集成测试)
def test_rolls_back_uncommitted_work_by_default(session_factory):
    uow = unit_of_work.SqlAlchemyUnitOfWork(session_factory)
    with uow:
        insert_batch(uow.session, "batch1", "MEDIUM-PLINTH", 100, None)

    new_session = session_factory()
    rows = list(new_session.execute('SELECT * FROM "batches"'))
    assert rows == []


def test_rolls_back_on_error(session_factory):
    class MyException(Exception):
        pass

    uow = unit_of_work.SqlAlchemyUnitOfWork(session_factory)
    with pytest.raises(MyException):
        with uow:
            insert_batch(uow.session, "batch1", "LARGE-FORK", 100, None)
            raise MyException()

    new_session = session_factory()
    rows = list(new_session.execute('SELECT * FROM "batches"'))
    assert rows == []
Tip
We haven’t shown it here, but it can be worth testing some of the more "obscure" database behavior, like transactions, against the "real" database—that is, the same engine. For now, we’re getting away with using SQLite instead of Postgres, but in [chapter_07_aggregate], we’ll switch some of the tests to using the real database. It’s convenient that our UoW class makes that easy! 我们在这里没有展示,但测试一些更“晦涩”的数据库行为(比如事务)与“真实”数据库的交互可能是值得的——也就是说,使用相同的引擎。 目前,我们暂时使用 SQLite 而不是 Postgres,但在 [chapter_07_aggregate] 中,我们会将部分测试切换为使用真实数据库。 很方便的是,我们的 UoW 类让这一切变得简单!

Explicit Versus Implicit Commits

显式提交与隐式提交

Now we briefly digress on different ways of implementing the UoW pattern.

现在我们将简要讨论实现工作单元模式的不同方式。

We could imagine a slightly different version of the UoW that commits by default and rolls back only if it spots an exception:

我们可以设想一种稍有不同的工作单元实现,它默认提交,并且仅在发现异常时回滚:

Example 9. A UoW with implicit commit…​ (src/allocation/unit_of_work.py)(一个具有隐式提交的工作单元…​)
class AbstractUnitOfWork(abc.ABC):

    def __enter__(self):
        return self

    def __exit__(self, exn_type, exn_value, traceback):
        if exn_type is None:
            self.commit()  #(1)
        else:
            self.rollback()  #(2)
  1. Should we have an implicit commit in the happy path? 我们是否应该在正常路径中使用隐式提交?

  2. And roll back only on exception? 并仅在发生异常时执行回滚?

It would allow us to save a line of code and to remove the explicit commit from our client code:

这将使我们节省一行代码,并从客户端代码中移除显式提交的操作:

Example 10. ...would save us a line of code (src/allocation/service_layer/services.py)(…​会为我们节省一行代码)
def add_batch(ref: str, sku: str, qty: int, eta: Optional[date], uow):
    with uow:
        uow.batches.add(model.Batch(ref, sku, qty, eta))
        # uow.commit()

This is a judgment call, but we tend to prefer requiring the explicit commit so that we have to choose when to flush state.

这是一种判断上的选择,但我们倾向于要求显式提交,这样我们就必须明确地选择何时刷新状态。

Although we use an extra line of code, this makes the software safe by default. The default behavior is to not change anything. In turn, that makes our code easier to reason about because there’s only one code path that leads to changes in the system: total success and an explicit commit. Any other code path, any exception, any early exit from the UoW’s scope leads to a safe state.

尽管我们多用了一行代码,但这使得软件在默认情况下是安全的。默认的行为是 不做任何更改。反过来,这让我们的代码更容易理解, 因为只有一条代码路径会导致系统发生更改:完全成功并显式提交。任何其他代码路径、任何异常、任何提前退出工作单元范围的情况都不会导致不安全的状态。

Similarly, we prefer to roll back by default because it’s easier to understand; this rolls back to the last commit, so either the user did one, or we blow their changes away. Harsh but simple.

同样地,我们倾向于默认执行回滚,因为这样更容易理解;这会回滚到上一次提交的状态,所以要么用户进行了提交,要么我们就丢弃他们的更改。 虽然严格,但却简单明了。

Examples: Using UoW to Group Multiple Operations into an Atomic Unit

示例:使用工作单元将多个操作组合成一个原子单元

Here are a few examples showing the Unit of Work pattern in use. You can see how it leads to simple reasoning about what blocks of code happen together.

以下是一些展示工作单元模式使用的示例。你可以看到它如何让我们能够简单地推理哪些代码块会一同执行。

Example 1: Reallocate

示例 1:重新分配

Suppose we want to be able to deallocate and then reallocate orders:

假设我们希望能够先取消分配订单,然后重新分配订单:

Example 11. Reallocate service function(重新分配服务函数)
def reallocate(
    line: OrderLine,
    uow: AbstractUnitOfWork,
) -> str:
    with uow:
        batch = uow.batches.get(sku=line.sku)
        if batch is None:
            raise InvalidSku(f'Invalid sku {line.sku}')
        batch.deallocate(line)  #(1)
        allocate(line)  #(2)
        uow.commit()
  1. If deallocate() fails, we don’t want to call allocate(), obviously. 显然,如果 deallocate() 失败,我们不希望调用 allocate()

  2. If allocate() fails, we probably don’t want to actually commit the deallocate() either. 如果 allocate() 失败,我们可能也不希望实际提交 deallocate() 的操作。

Example 2: Change Batch Quantity

示例 2:更改批次数量

Our shipping company gives us a call to say that one of the container doors opened, and half our sofas have fallen into the Indian Ocean. Oops!

我们的运输公司打电话告诉我们,其中一个集装箱的门打开了,我们一半的沙发掉进了印度洋。糟糕!

Example 12. Change quantity(更改数量)
def change_batch_quantity(
    batchref: str, new_qty: int,
    uow: AbstractUnitOfWork,
):
    with uow:
        batch = uow.batches.get(reference=batchref)
        batch.change_purchased_quantity(new_qty)
        while batch.available_quantity < 0:
            line = batch.deallocate_one()  #(1)
        uow.commit()
  1. Here we may need to deallocate any number of lines. If we get a failure at any stage, we probably want to commit none of the changes. 在这里,我们可能需要释放任意数量的行。如果在任何阶段出现失败,我们可能希望不提交任何更改。

Tidying Up the Integration Tests

整理集成测试

We now have three sets of tests, all essentially pointing at the database: test_orm.py, test_repository.py, and test_uow.py. Should we throw any away?

我们现在有三组测试,它们本质上都指向数据库:test_orm.pytest_repository.pytest_uow.py。我们应该丢弃其中的某些测试吗?

└── tests
    ├── conftest.py
    ├── e2e
    │   └── test_api.py
    ├── integration
    │   ├── test_orm.py
    │   ├── test_repository.py
    │   └── test_uow.py
    ├── pytest.ini
    └── unit
        ├── test_allocate.py
        ├── test_batches.py
        └── test_services.py

You should always feel free to throw away tests if you think they’re not going to add value longer term. We’d say that test_orm.py was primarily a tool to help us learn SQLAlchemy, so we won’t need that long term, especially if the main things it’s doing are covered in test_repository.py. That last test, you might keep around, but we could certainly see an argument for just keeping everything at the highest possible level of abstraction (just as we did for the unit tests).

如果你认为某些测试从长期来看不会带来价值,你完全可以随时将它们删除。我们会说 test_orm.py 主要是帮助我们学习 SQLAlchemy 的工具, 因此从长期来看我们并不需要它,特别是当它的主要功能已经被 test_repository.py 所覆盖时。而对于最后的那个测试 (test_uow.py), 你可能会选择保留,但我们也完全可以接受只保留尽可能高层次抽象的测试(就像我们对单元测试所做的一样)的观点。

Exercise for the Reader(读者练习)

For this chapter, probably the best thing to try is to implement a UoW from scratch. The code, as always, is on GitHub. You could either follow the model we have quite closely, or perhaps 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. If you feel like going all-functional rather than messing about with all these classes, you could use @contextmanager from contextlib.

对于本章来说,可能最好的尝试是从头实现一个工作单元。 代码一如既往地可以在 GitHub 上 找到。 你可以选择非常贴近我们现有的示例模型,也可以尝试将 UoW 与上下文管理器分离开来进行实验(工作单元的职责是 commit()rollback() 并提供 .batches 仓储, 而上下文管理器的职责是进行初始化,然后在退出时执行提交或回滚操作)。如果你想完全采用函数式的方式,而不是处理这些类,你可以使用 contextlib 中的 @contextmanager

We’ve stripped out both the actual UoW and the fakes, as well as paring back the abstract UoW. Why not send us a link to your repo if you come up with something you’re particularly proud of?

我们已经剥离了实际的工作单元和伪对象,同时也简化了抽象工作单元。如果你设计出令自己特别自豪的东西,为什么不将你的仓储链接发送给我们呢?

Tip
This is another example of the lesson from [chapter_05_high_gear_low_gear]: as we build better abstractions, we can move our tests to run against them, which leaves us free to change the underlying details. 这是来自[chapter_05_high_gear_low_gear]的一课的另一个例子:当我们构建出更好的抽象时, 我们可以让测试针对这些抽象运行,这使得我们能够自由地更改底层的细节。

Wrap-Up

总结

Hopefully we’ve convinced you that the Unit of Work pattern is useful, and that the context manager is a really nice Pythonic way of visually grouping code into blocks that we want to happen atomically.

希望我们已经让你相信,工作单元模式是有用的,并且上下文管理器是一种非常优雅的 Python 风格方式, 可以直观地将我们希望原子化执行的代码分组到块中。

This pattern is so useful, in fact, that SQLAlchemy already uses a UoW in the shape of the Session object. The Session object in SQLAlchemy is the way that your application loads data from the database.

事实上,这种模式非常有用,以至于 SQLAlchemy 已经在其 Session 对象中实现了一个工作单元。在 SQLAlchemy 中, Session 对象是你的应用程序从数据库加载数据的方式。

Every time you load a new entity from the database, the session begins to track changes to the entity, and when the session is flushed, all your changes are persisted together. Why do we go to the effort of abstracting away the SQLAlchemy session if it already implements the pattern we want?

每次你从数据库加载一个新的实体时,Session 会开始 追踪 该实体的更改,而当 Session刷新(flushed) 时, 所有的更改都会被一起持久化。那么,既然 SQLAlchemy 的 Session 已经实现了我们想要的模式,为什么我们还要费力地对它进行抽象呢?

Table 1. Unit of Work pattern: the trade-offs(工作单元模式:权衡取舍)
Pros(优点) Cons(缺点)
  • We have a nice abstraction over the concept of atomic operations, and the context manager makes it easy to see, visually, what blocks of code are grouped together atomically. 我们在原子操作的概念上拥有了一个优雅的抽象,上下文管理器使我们能够直观地看到哪些代码块被归组到了一起以原子方式执行。

  • We have explicit control over when a transaction starts and finishes, and our application fails in a way that is safe by default. We never have to worry that an operation is partially committed. 我们对事务的开始和结束有明确的控制,并且我们的应用程序默认情况下能以一种安全的方式失败。我们永远不必担心某个操作只被部分提交。

  • It’s a nice place to put all your repositories so client code can access them. 这是一个放置所有仓储的好地方,这样客户端代码就可以访问它们。

  • As you’ll see in later chapters, atomicity isn’t only about transactions; it can help us work with events and the message bus. 正如你将在后续章节中看到的,原子性不仅仅与事务有关;它还可以帮助我们处理事件和消息总线。

  • Your ORM probably already has some perfectly good abstractions around atomicity. SQLAlchemy even has context managers. You can go a long way just passing a session around. 你的 ORM 可能已经有一些非常好的关于原子性的抽象。SQLAlchemy 甚至提供了上下文管理器。仅仅通过传递一个 session,你也能实现很多功能。

  • We’ve made it look easy, but you have to think quite carefully about things like rollbacks, multithreading, and nested transactions. Perhaps just sticking to what Django or Flask-SQLAlchemy gives you will keep your life simpler. 虽然我们让这一切看起来很简单,但你必须非常仔细地考虑诸如回滚、多线程以及嵌套事务等问题。 也许只是坚持使用 Django 或 Flask-SQLAlchemy 提供的功能会让你的生活更简单一些。

For one thing, the Session API is rich and supports operations that we don’t want or need in our domain. Our UnitOfWork simplifies the session to its essential core: it can be started, committed, or thrown away.

首先,Session 的 API 非常丰富,并且支持我们在领域中不需要或不想要的操作。 而我们的 UnitOfWork 将会话简化为其核心本质:它可以被启动、提交或丢弃。

For another, we’re using the UnitOfWork to access our Repository objects. This is a neat bit of developer usability that we couldn’t do with a plain SQLAlchemy Session.

另一方面,我们使用 UnitOfWork 来访问我们的 Repository 对象。这是一种简洁的开发者易用性设计, 而这是单纯使用 SQLAlchemy 的 Session 无法实现的。

Unit of Work Pattern Recap(工作单元模式回顾)

The Unit of Work pattern is an abstraction around data integrity(工作单元模式是围绕数据完整性的一种抽象)

It helps to enforce the consistency of our domain model, and improves performance, by letting us perform a single flush operation at the end of an operation. 它通过允许我们在操作结束时执行一次 刷新(flush) 操作,帮助我们强制维护领域模型的一致性,并提高性能。

It works closely with the Repository and Service Layer patterns(它与仓储模式和服务层模式紧密协作)

The Unit of Work pattern completes our abstractions over data access by representing atomic updates. Each of our service-layer use cases runs in a single unit of work that succeeds or fails as a block. 工作单元模式通过表示原子更新来完善我们对数据访问的抽象。我们的每个服务层用例都运行在一个单独的工作单元中,该工作单元要么整体成功,要么整体失败。

This is a lovely case for a context manager(这正是一个上下文管理器的绝佳应用场景)

Context managers are an idiomatic way of defining scope in Python. We can use a context manager to automatically roll back our work at the end of a request, which means the system is safe by default. 上下文管理器是定义 Python 中作用域的一种惯用方式。我们可以使用上下文管理器在请求结束时自动回滚我们的工作,这意味着系统默认是安全的。

SQLAlchemy already implements this pattern(SQLAlchemy 已经实现了这种模式)

We introduce an even simpler abstraction over the SQLAlchemy Session object in order to "narrow" the interface between the ORM and our code. This helps to keep us loosely coupled. 我们在 SQLAlchemy 的 Session 对象之上引入了一个更简单的抽象,以便“收窄” ORM 和我们的代码之间的接口。这有助于保持松耦合。

Lastly, we’re motivated again by the dependency inversion principle: our service layer depends on a thin abstraction, and we attach a concrete implementation at the outside edge of the system. This lines up nicely with SQLAlchemy’s own recommendations:

最后,我们再次受到依赖倒置原则的推动:我们的服务层依赖于一个精简的抽象,而具体的实现则附加在系统的外围。这与 SQLAlchemy 自身的 推荐 非常契合:

Keep the life cycle of the session (and usually the transaction) separate and external. The most comprehensive approach, recommended for more substantial applications, will try to keep the details of session, transaction, and exception management as far as possible from the details of the program doing its work.

将会话(以及通常是事务)的生命周期分离并置于外部。对于更复杂的应用程序,推荐采用最全面的方法, 该方法将尽量让会话、事务以及异常管理的细节远离实际程序逻辑的细节。

— SQLALchemy "Session Basics" Documentation

1. You may have come across the use of the word collaborators to describe objects that work together to achieve a goal. The unit of work and the repository are a great example of collaborators in the object-modeling sense. In responsibility-driven design, clusters of objects that collaborate in their roles are called object neighborhoods, which is, in our professional opinion, totally adorable.