在 Django 中使用 仓储 和工作单元模式
Suppose you wanted to use Django instead of SQLAlchemy and Flask. How might things look? The first thing is to choose where to install it. We put it in a separate package next to our main allocation code:
假设你想使用 Django 来替代 SQLAlchemy 和 Flask。那么,应该如何实现呢?首先,需要选择在哪里安装它。 我们将其放在一个与我们的主要分配代码相邻的独立包中:
├── src
│ ├── allocation
│ │ ├── __init__.py
│ │ ├── adapters
│ │ │ ├── __init__.py
...
│ ├── djangoproject
│ │ ├── alloc
│ │ │ ├── __init__.py
│ │ │ ├── apps.py
│ │ │ ├── migrations
│ │ │ │ ├── 0001_initial.py
│ │ │ │ └── __init__.py
│ │ │ ├── models.py
│ │ │ └── views.py
│ │ ├── django_project
│ │ │ ├── __init__.py
│ │ │ ├── settings.py
│ │ │ ├── urls.py
│ │ │ └── wsgi.py
│ │ └── manage.py
│ └── setup.py
└── tests
├── conftest.py
├── e2e
│ └── test_api.py
├── integration
│ ├── test_repository.py
...|
Tip
|
The code for this appendix is in the appendix_django branch on GitHub: 本附录的代码位于 appendix_django 分支 在 GitHub 上: git clone https://github.com/cosmicpython/code.git cd code git checkout appendix_django Code examples follows on from the end of [chapter_06_uow]. 代码示例接续自 [chapter_06_uow] 的结尾。 |
使用 Django 的仓储模式
We used a plugin called
pytest-django to help with test
database management.
我们使用了一个名为 pytest-django 的插件来帮助管理测试数据库。
Rewriting the first repository test was a minimal change—just rewriting some raw SQL with a call to the Django ORM/QuerySet language:
重写第一个仓储测试是一个最小化的改动——只是用调用 Django ORM/QuerySet 语言来重写了一些原始 SQL:
from djangoproject.alloc import models as django_models
@pytest.mark.django_db
def test_repository_can_save_a_batch():
batch = model.Batch("batch1", "RUSTY-SOAPDISH", 100, eta=date(2011, 12, 25))
repo = repository.DjangoRepository()
repo.add(batch)
[saved_batch] = django_models.Batch.objects.all()
assert saved_batch.reference == batch.reference
assert saved_batch.sku == batch.sku
assert saved_batch.qty == batch._purchased_quantity
assert saved_batch.eta == batch.etaThe second test is a bit more involved since it has allocations, but it is still made up of familiar-looking Django code:
第二个测试稍微复杂一些,因为它涉及分配,但它仍然由看起来熟悉的 Django 代码组成:
@pytest.mark.django_db
def test_repository_can_retrieve_a_batch_with_allocations():
sku = "PONY-STATUE"
d_line = django_models.OrderLine.objects.create(orderid="order1", sku=sku, qty=12)
d_batch1 = django_models.Batch.objects.create(
reference="batch1", sku=sku, qty=100, eta=None
)
d_batch2 = django_models.Batch.objects.create(
reference="batch2", sku=sku, qty=100, eta=None
)
django_models.Allocation.objects.create(line=d_line, batch=d_batch1)
repo = repository.DjangoRepository()
retrieved = repo.get("batch1")
expected = model.Batch("batch1", sku, 100, eta=None)
assert retrieved == expected # Batch.__eq__ only compares reference
assert retrieved.sku == expected.sku
assert retrieved._purchased_quantity == expected._purchased_quantity
assert retrieved._allocations == {
model.OrderLine("order1", sku, 12),
}Here’s how the actual repository ends up looking:
实际的仓储最终如下所示:
class DjangoRepository(AbstractRepository):
def add(self, batch):
super().add(batch)
self.update(batch)
def update(self, batch):
django_models.Batch.update_from_domain(batch)
def _get(self, reference):
return (
django_models.Batch.objects.filter(reference=reference)
.first()
.to_domain()
)
def list(self):
return [b.to_domain() for b in django_models.Batch.objects.all()]You can see that the implementation relies on the Django models having some custom methods for translating to and from our domain model.[1]
你可以看到,该实现依赖于 Django 模型中一些自定义方法来在我们的领域模型之间进行转换。脚注: DRY-Python 项目的开发者构建了一个名为 mappers 的工具, 看起来它可能有助于减少此类代码的样板。
在 Django ORM 类上定义自定义方法用于在我们的领域模型之间进行转换
Those custom methods look something like this:
这些自定义方法看起来是这样的:
from django.db import models
from allocation.domain import model as domain_model
class Batch(models.Model):
reference = models.CharField(max_length=255)
sku = models.CharField(max_length=255)
qty = models.IntegerField()
eta = models.DateField(blank=True, null=True)
@staticmethod
def update_from_domain(batch: domain_model.Batch):
try:
b = Batch.objects.get(reference=batch.reference) #(1)
except Batch.DoesNotExist:
b = Batch(reference=batch.reference) #(1)
b.sku = batch.sku
b.qty = batch._purchased_quantity
b.eta = batch.eta #(2)
b.save()
b.allocation_set.set(
Allocation.from_domain(l, b) #(3)
for l in batch._allocations
)
def to_domain(self) -> domain_model.Batch:
b = domain_model.Batch(
ref=self.reference, sku=self.sku, qty=self.qty, eta=self.eta
)
b._allocations = set(
a.line.to_domain()
for a in self.allocation_set.all()
)
return b
class OrderLine(models.Model):
#...-
For value objects,
objects.get_or_createcan work, but for entities, you probably need an explicit try-get/except to handle the upsert.[2] 对于值对象,objects.get_or_create可以正常工作,但对于实体,你可能需要显式的 try-get/except 来处理 upsert(更新或插入)。脚注:@mr-bo-jangles提出你或许可以使用update_or_create,但这超出了我们对 Django 的掌握范围。 -
We’ve shown the most complex example here. If you do decide to do this, be aware that there will be boilerplate! Thankfully it’s not very complex boilerplate. 我们在这里展示了最复杂的示例。如果你决定这样做,请注意会有一些样板代码!不过值得庆幸的是,这些样板代码并不复杂。
-
Relationships also need some careful, custom handling. 关系也需要一些仔细而定制化的处理。
|
Note
|
As in [chapter_02_repository], we use dependency inversion. The ORM (Django) depends on the model and not the other way around. 与 [chapter_02_repository] 中一样,我们使用了依赖反转原则。 ORM(Django)依赖于模型,而不是反过来。 |
使用 Django 的工作单元模式
The tests don’t change too much:
测试并没有发生太大的变化:
def insert_batch(ref, sku, qty, eta): #(1)
django_models.Batch.objects.create(reference=ref, sku=sku, qty=qty, eta=eta)
def get_allocated_batch_ref(orderid, sku): #(1)
return django_models.Allocation.objects.get(
line__orderid=orderid, line__sku=sku
).batch.reference
@pytest.mark.django_db(transaction=True)
def test_uow_can_retrieve_a_batch_and_allocate_to_it():
insert_batch("batch1", "HIPSTER-WORKBENCH", 100, None)
uow = unit_of_work.DjangoUnitOfWork()
with uow:
batch = uow.batches.get(reference="batch1")
line = model.OrderLine("o1", "HIPSTER-WORKBENCH", 10)
batch.allocate(line)
uow.commit()
batchref = get_allocated_batch_ref("o1", "HIPSTER-WORKBENCH")
assert batchref == "batch1"
@pytest.mark.django_db(transaction=True) #(2)
def test_rolls_back_uncommitted_work_by_default():
...
@pytest.mark.django_db(transaction=True) #(2)
def test_rolls_back_on_error():
...-
Because we had little helper functions in these tests, the actual main bodies of the tests are pretty much the same as they were with SQLAlchemy. 由于我们在这些测试中使用了一些辅助函数,测试的主要主体部分实际上与使用 SQLAlchemy 时几乎相同。
-
The
pytest-djangomark.django_db(transaction=True)is required to test our custom transaction/rollback behaviors. 为了测试我们自定义的事务/回滚行为,需要使用pytest-django的mark.django_db(transaction=True)。
And the implementation is quite simple, although it took me a few tries to find which invocation of Django’s transaction magic would work:
实现相当简单,尽管我花了几次尝试才找到能够发挥作用的 Django 事务机制的调用方式:
class DjangoUnitOfWork(AbstractUnitOfWork):
def __enter__(self):
self.batches = repository.DjangoRepository()
transaction.set_autocommit(False) #(1)
return super().__enter__()
def __exit__(self, *args):
super().__exit__(*args)
transaction.set_autocommit(True)
def commit(self):
for batch in self.batches.seen: #(3)
self.batches.update(batch) #(3)
transaction.commit() #(2)
def rollback(self):
transaction.rollback() #(2)-
set_autocommit(False)was the best way to tell Django to stop automatically committing each ORM operation immediately, and to begin a transaction.set_autocommit(False)是告诉 Django 停止立即自动提交每次 ORM 操作并开始一个事务的最佳方式。 -
Then we use the explicit rollback and commits. 然后我们使用显式的回滚和提交操作。
-
One difficulty: because, unlike with SQLAlchemy, we’re not instrumenting the domain model instances themselves, the
commit()command needs to explicitly go through all the objects that have been touched by every repository and manually update them back to the ORM. 一个难点是:与使用 SQLAlchemy 不同,我们并没有对领域模型实例本身进行操作,因此commit()命令需要显式地遍历每个仓储操作过的所有对象, 并手动将它们更新回 ORM。
API:Django 视图是适配器
The Django views.py file ends up being almost identical to the old flask_app.py, because our architecture means it’s a very thin wrapper around our service layer (which didn’t change at all, by the way):
Django 的 views.py 文件最终与之前的 flask_app.py 几乎完全相同, 因为我们的架构使其成为服务层的一个非常薄的封装(顺便说一下,服务层完全没有改变):
os.environ["DJANGO_SETTINGS_MODULE"] = "djangoproject.django_project.settings"
django.setup()
@csrf_exempt
def add_batch(request):
data = json.loads(request.body)
eta = data["eta"]
if eta is not None:
eta = datetime.fromisoformat(eta).date()
services.add_batch(
data["ref"], data["sku"], data["qty"], eta,
unit_of_work.DjangoUnitOfWork(),
)
return HttpResponse("OK", status=201)
@csrf_exempt
def allocate(request):
data = json.loads(request.body)
try:
batchref = services.allocate(
data["orderid"],
data["sku"],
data["qty"],
unit_of_work.DjangoUnitOfWork(),
)
except (model.OutOfStock, services.InvalidSku) as e:
return JsonResponse({"message": str(e)}, status=400)
return JsonResponse({"batchref": batchref}, status=201)为什么这一切都如此困难?
OK, it works, but it does feel like more effort than Flask/SQLAlchemy. Why is that?
好的,它可以工作,但确实感觉比 Flask/SQLAlchemy 更费力。为什么会这样呢?
The main reason at a low level is because Django’s ORM doesn’t work in the same
way. We don’t have an equivalent of the SQLAlchemy classical mapper, so our
ActiveRecord and our domain model can’t be the same object. Instead we have to
build a manual translation layer behind the repository. That’s more
work (although once it’s done, the ongoing maintenance burden shouldn’t be too
high).
从底层来看,主要原因是 Django 的 ORM 工作方式不同。我们没有与 SQLAlchemy 的经典映射器等价的功能,
因此我们的 ActiveRecord 和领域模型不能是同一个对象。相反,我们必须在仓储后面构建一个手动的转换层。这确实需要更多的工作(不过一旦完成,
后续的维护负担应该不会太高)。
Because Django is so tightly coupled to the database, you have to use helpers
like pytest-django and think carefully about test databases, right from
the very first line of code, in a way that we didn’t have to when we started
out with our pure domain model.
因为 Django 与数据库的耦合非常紧密,所以你必须使用类似 pytest-django 这样的辅助工具,并从第一行代码开始就仔细考虑测试数据库的设置,
这是我们在使用纯领域模型开始时所不需要处理的。
But at a higher level, the entire reason that Django is so great is that it’s designed around the sweet spot of making it easy to build CRUD apps with minimal boilerplate. But the entire thrust of our book is about what to do when your app is no longer a simple CRUD app.
但从更高的层面来看,Django 之所以如此出色,完全是因为它围绕着简化构建 CRUD 应用的最佳方式设计,且所需的样板代码极少。 但我们这本书的核心讨论点是,当你的应用不再是一个简单的 CRUD 应用时,该怎么办。
At that point, Django starts hindering more than it helps. Things like the Django admin, which are so awesome when you start out, become actively dangerous if the whole point of your app is to build a complex set of rules and modeling around the workflow of state changes. The Django admin bypasses all of that.
此时,Django 帮助的作用开始被它带来的阻碍所抵消。像 Django Admin 这样的功能,在开始时非常出色, 但如果你的应用的核心在于围绕状态变更的工作流构建一套复杂的规则和模型,那么它就会变得极其危险。因为 Django Admin 会绕过这些规则和逻辑。
如果你已经在使用 Django,该怎么办
So what should you do if you want to apply some of the patterns in this book to a Django app? We’d say the following:
那么,如果你想将本书中的一些模式应用到一个 Django 应用中,你应该怎么做呢?我们建议如下:
-
The Repository and Unit of Work patterns are going to be quite a lot of work. The main thing they will buy you in the short term is faster unit tests, so evaluate whether that benefit feels worth it in your case. In the longer term, they decouple your app from Django and the database, so if you anticipate wanting to migrate away from either of those, Repository and UoW are a good idea. 仓储模式和工作单元模式会带来相当多的工作量。从短期来看,它们主要为你带来的好处是更快的单元测试,因此你需要评估这种好处是否对你来说值得。 从长期来看,它们会将你的应用程序与 Django 和数据库解耦,所以如果你预计可能需要从两者中的任何一个迁移开, 使用仓储模式和工作单元模式是一个不错的选择。
-
The Service Layer pattern might be of interest if you’re seeing a lot of duplication in your views.py. It can be a good way of thinking about your use cases separately from your web endpoints. 如果你在 views.py 文件中看到大量的代码重复,那么服务层模式可能会引起你的兴趣。它是一种将你的用例与 Web 端点分开思考的好方法。
-
You can still theoretically do DDD and domain modeling with Django models, tightly coupled as they are to the database; you may be slowed by migrations, but it shouldn’t be fatal. So as long as your app is not too complex and your tests not too slow, you may be able to get something out of the fat models approach: push as much logic down to your models as possible, and apply patterns like Entity, Value Object, and Aggregate. However, see the following caveat. 理论上,即使 Django 模型与数据库紧密耦合,你仍然可以使用 DDD(领域驱动设计)和领域建模;虽然迁移过程可能会拖慢你的进度,但这不至于致命。 所以只要你的应用程序不是太复杂,测试也不是太慢,你或许可以从 胖模型 方法中获益:尽可能将逻辑下放到模型中, 并应用如实体(Entity)、值对象(Value Object)和聚合(Aggregate)等模式。然而,请注意以下的注意事项。
With that said, word in the Django community is that people find that the fat models approach runs into scalability problems of its own, particularly around managing interdependencies between apps. In those cases, there’s a lot to be said for extracting out a business logic or domain layer to sit between your views and forms and your models.py, which you can then keep as minimal as possible.
话虽如此, 在 Django 社区的反馈 表明,人们发现胖模型方法本身会遇到可扩展性问题,特别是在管理应用程序之间的相互依赖方面。 在这些情况下,将业务逻辑或领域层提取出来,置于视图和表单与 models.py 之间,有很多好处。而且,这也让你的 models.py 可以尽量保持精简。
渐进式的步骤
Suppose you’re working on a Django project that you’re not sure is going to get complex enough to warrant the patterns we recommend, but you still want to put a few steps in place to make your life easier, both in the medium term and if you want to migrate to some of our patterns later. Consider the following:
假设你正在开发一个 Django 项目,而你不确定该项目是否会变得足够复杂以至于需要使用我们推荐的模式,但你仍然希望采取一些步骤, 使你的工作在中期更轻松一些,并且如果将来想迁移到我们的一些模式也会更方便。可以考虑以下建议:
-
One piece of advice we’ve heard is to put a logic.py into every Django app from day one. This gives you a place to put business logic, and to keep your forms, views, and models free of business logic. It can become a stepping-stone for moving to a fully decoupled domain model and/or service layer later. 我们听过的一条建议是,从第一天开始就在每个 Django 应用中创建一个 logic.py 文件。这为你提供了一个放置业务逻辑的地方, 同时可以让你的表单、视图和模型中不包含业务逻辑。它可以成为将来迁移到完全解耦的领域模型和/或服务层的一个踏脚石。
-
A business-logic layer might start out working with Django model objects and only later become fully decoupled from the framework and work on plain Python data structures. 业务逻辑层可能一开始是与 Django 模型对象一起工作的,而只有在之后才完全与框架解耦,转而使用纯粹的 Python 数据结构。
-
For the read side, you can get some of the benefits of CQRS by putting reads into one place, avoiding ORM calls sprinkled all over the place. 在读取方面,你可以通过将读取操作集中到一个地方来获得一些 CQRS 的好处,避免 ORM 调用分散在各处。
-
When separating out modules for reads and modules for domain logic, it may be worth decoupling yourself from the Django apps hierarchy. Business concerns will cut across them. 当将读取模块和领域逻辑模块分离时,值得考虑让自己从 Django 的应用层次结构中解耦。业务需求通常会跨越这些应用模块。
|
Note
|
We’d like to give a shout-out to David Seddon and Ashia Zawaduk for talking through some of the ideas in this appendix. They did their best to stop us from saying anything really stupid about a topic we don’t really have enough personal experience of, but they may have failed. 我们要向 David Seddon 和 Ashia Zawaduk 表示感谢,感谢他们与我们一起讨论了本附录中的一些想法。 他们尽了最大的努力阻止我们在一个我们自己没有足够经验的话题上说出任何非常愚蠢的话,不过他们可能未能完全做到。 |
For more thoughts and actual lived experience dealing with existing applications, refer to the epilogue.
有关处理现有应用程序的更多想法和实际经验,请参阅 尾声。
@mr-bo-jangles suggested you might be able to use update_or_create, but that’s beyond our Django-fu.