在Django中将业务逻辑和数据访问分离。
在Django中将业务逻辑和数据访问分离。
我正在使用Django编写一个项目,我发现80%的代码在文件models.py
中。这段代码很令人困惑,一段时间后,我就不再理解实际发生了什么。
以下是我在困扰的问题:
- 我认为将我的模型级别(原本只应负责与来自数据库的数据打交道)也用于发送电子邮件、与其他服务的API通信等很丑陋。
- 另外,我认为将业务逻辑放在视图中是不可接受的,因为这样变得很难控制。例如,在我的应用程序中,至少有三种方法可以创建新的
User
实例,但从技术上讲,它们应当统一创建。 - 我不总是注意到我的模型的方法和属性何时变得不确定并产生副作用。
这里是一个简单的例子。起初,User
模型是这样的:
class User(db.Models): def get_present_name(self): return self.name or 'Anonymous' def activate(self): self.status = 'activated' self.save()
随着时间的推移,它变成了这样:
class User(db.Models): def get_present_name(self): # property became non-deterministic in terms of database # data is taken from another service by api return remote_api.request_user_name(self.uid) or 'Anonymous' def activate(self): # method now has a side effect (send message to user) self.status = 'activated' self.save() send_mail('Your account is activated!', '…', [self.email])
我想要的是在代码中分离实体:
- 数据库级别实体,即数据库级别逻辑:我的应用程序存储什么类型的数据?
- 应用程序级别实体,即业务级别逻辑:我的应用程序具有哪些功能?
如何实现这种方法的良好实践并应用于Django?
我通常在视图和模型之间实现一个服务层。这就像您项目的API,可以让您对情况有一个良好的全局视图。我从我的同事那里继承了这种使用层次结构技术的做法,他在Java项目中(例如JSF)经常使用此方法,例如:
models.py
class Book: author = models.ForeignKey(User) title = models.CharField(max_length=125) class Meta: app_label = "library"
services.py
from library.models import Book def get_books(limit=None, **filters): """ simple service function for retrieving books can be widely extended """ return Book.objects.filter(**filters)[:limit] # list[:None] will return the entire list
views.py
from library.services import get_books class BookListView(ListView): """ simple view, e.g. implement a _build and _apply filters function """ queryset = get_books()
请注意,我通常将模型、视图和服务拆分成模块级别,并根据项目的规模进一步分开。
看起来你在询问数据模型和域模型的区别,后者是您的最终用户所感知的业务逻辑和实体的地方,而前者是存储实际数据的地方。
此外,我把你问题的第三部分解释为:如何注意保持这些模型的分开。
这是两个非常不同的概念,但很难将它们分开。但是,有一些常见的模式和工具可用于此目的。
关于域模型
您需要认识到的第一件事是,您的域模型实际上并不涉及数据; 它涉及动作和问题,例如“激活此用户”,“停用此用户”,“哪些用户当前处于激活状态?”以及“此用户的姓名是什么?”。在经典术语中:它涉及查询和命令。
以命令方式思考
让我们先看看您的示例中的命令:“激活此用户”和“停用此用户”。命令的好处在于它们可以通过小型的“给出-当-然后”场景轻松表达:
给出一个非活动用户
当管理员激活此用户时
则该用户变为活动状态
并且向用户发送确认电子邮件
并且将项添加到系统日志中
(等等等等)
这样的场景有助于了解单个命令如何影响您基础设施的不同部分- 在本例中为您的数据库(某种“活动”flag),您的邮件服务器,您的系统日志等。
这样的场景还真的可以帮助您建立一个测试驱动开发环境。
最后,思考命令确实有助于您创建一个面向任务的应用程序。您的用户会感激这一点 🙂
表达命令
Django提供了两种表达命令的简单方法;它们都是可用的选项,并且混合使用这两种方法并不常见。
服务层
服务模块已由@Hedde描述。在这里,您定义一个单独的模块,每个命令都表示为一个函数。
services.py
def activate_user(user_id): user = User.objects.get(pk=user_id) # set active flag user.active = True user.save() # mail user send_mail(...) # etc etc
使用表单
另一种方法是为每个命令使用Django表单。我更喜欢这种方法,因为它结合了多个密切相关的方面:
- 命令的执行 (它是做什么的?)
- 命令参数的验证 (它能做到这一点吗?)
- 命令的呈现 (我该怎么做?)
forms.py
class ActivateUserForm(forms.Form): user_id = IntegerField(widget = UsernameSelectWidget, verbose_name="Select a user to activate") # the username select widget is not a standard Django widget, I just made it up def clean_user_id(self): user_id = self.cleaned_data['user_id'] if User.objects.get(pk=user_id).active: raise ValidationError("This user cannot be activated") # you can also check authorizations etc. return user_id def execute(self): """ This is not a standard method in the forms API; it is intended to replace the 'extract-data-from-form-in-view-and-do-stuff' pattern by a more testable pattern. """ user_id = self.cleaned_data['user_id'] user = User.objects.get(pk=user_id) # set active flag user.active = True user.save() # mail user send_mail(...) # etc etc
思考查询
您的示例没有包含任何查询,因此我冒失地想出了一些有用的查询。我更喜欢使用术语“问题”,但查询是传统术语。有趣的查询是:“这个用户的名字是什么?”,“这个用户可以登录吗?”,“给我一个已停用用户列表”,以及“已停用用户的地理分布如何?”
在回答这些问题之前,您始终应该问自己这个问题,这是:
- 仅适用于模板的呈现查询,和/或
- 与执行命令相关的业务逻辑查询,和/或
- 报告查询。
呈现查询仅仅是为了改善用户界面。业务逻辑查询的答案直接影响您的命令执行。报告查询仅用于分析目的,并且具有较松的时间约束。这些类别并不是互相排斥的。
另一个问题是: "我对答案是否有完全的控制权?" 例如,在查询用户的姓名时(在这种情况下),我们对结果没有任何控制,因为我们依赖于外部 API。
进行查询
Django 中最基本的查询是使用 Manager 对象:
User.objects.filter(active=True)
当然,这仅在数据实际上在数据模型中表示时才起作用。并非总是如此。在这些情况下,您可以考虑以下选项。
自定义标记和过滤器
第一种替代方法适用于仅呈现的查询: 自定义标记和模板过滤器。
template.html
Welcome, {{ user|friendly_name }}
template_tags.py
@register.filter def friendly_name(user): return remote_api.get_cached_name(user.id)
查询方法
如果您的查询不仅仅是演示目的,您可以将查询添加到您的 services.py(如果您正在使用它),或者引入一个 queries.py 模块:
queries.py
def inactive_users(): return User.objects.filter(active=False) def users_called_publysher(): for user in User.objects.all(): if remote_api.get_cached_name(user.id) == "publysher": yield user
代理模型
代理模型在业务逻辑和报告的情况下非常有用。您基本上定义了您模型的一个增强子集。您可以通过覆盖 Manager 的基本 QuerySet 来覆盖 Manager 的基本 QuerySet。
models.py
class InactiveUserManager(models.Manager): def get_queryset(self): query_set = super(InactiveUserManager, self).get_queryset() return query_set.filter(active=False) class InactiveUser(User): """ >>> for user in InactiveUser.objects.all(): … assert user.active is False """ objects = InactiveUserManager() class Meta: proxy = True
查询模型
对于本质上复杂但经常执行的查询,有可能使用查询模型。查询模型是一种形式的去规范化,其中单个查询的相关数据存储在单独的模型中。当然,技巧在于将非规范化模型与主模型保持同步。如果完全控制变化,则只能使用查询模型。
models.py
class InactiveUserDistribution(models.Model): country = CharField(max_length=200) inactive_user_count = IntegerField(default=0)
第一种选择是在你的命令中更新这些模型。如果这些模型只被一个或两个命令更改,这非常有用。
forms.py
class ActivateUserForm(forms.Form): # see above def execute(self): # see above query_model = InactiveUserDistribution.objects.get_or_create(country=user.country) query_model.inactive_user_count -= 1 query_model.save()
更好的选择是使用自定义信号。这些信号当然是由你的命令发出的。信号的优点是你可以将多个查询模型与你的原始模型保持同步。此外,信号处理可以使用Celery或类似的框架卸载到后台任务中。
signals.py
user_activated = Signal(providing_args = ['user']) user_deactivated = Signal(providing_args = ['user'])
forms.py
class ActivateUserForm(forms.Form): # see above def execute(self): # see above user_activated.send_robust(sender=self, user=user)
models.py
class InactiveUserDistribution(models.Model): # see above @receiver(user_activated) def on_user_activated(sender, **kwargs): user = kwargs['user'] query_model = InactiveUserDistribution.objects.get_or_create(country=user.country) query_model.inactive_user_count -= 1 query_model.save()
保持清洁
使用这种方法时,确定你的代码是否保持干净变得非常容易。只需遵循以下准则:
- 我的模型是否包含执行数据库状态管理以外的方法?应该提取一个命令。
- 我的模型是否包含不映射到数据库字段的属性?应该提取一个查询。
- 我的模型是否引用不属于我的数据库基础设施(如邮件)的基础设施?应该提取命令。
对于视图也是如此(因为视图通常也存在相同的问题)。
- 我的视图是否主动管理数据库模型?应该提取一个命令。