问题:django中业务逻辑和数据访问的分离
我正在Django中编写一个项目,并且看到80%的代码在file中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中应用的方法?
回答 0
似乎您是在询问数据模型与域模型 –后者是您可以找到最终用户感知的业务逻辑和实体的地方,前者是您实际存储数据的地方。
此外,我将问题的第三部分解释为:如何注意到未能将这些模型分开的问题。
这是两个截然不同的概念,很难将它们分开。但是,有一些常见的模式和工具可用于此目的。
关于领域模型
您需要认识的第一件事是您的域模型并不是真正的数据。它涉及诸如“激活此用户”,“停用此用户”,“当前已激活哪些用户”和“该用户的名字是什么”之类的动作和问题。用经典术语来说:它是关于查询和命令的。
指挥思维
让我们从示例中的命令开始:“激活此用户”和“停用此用户”。关于命令的好处是,它们可以很容易地用小给定的情况来表示:
当管理员激活该用户时,将其指定为非活动用户,
则该用户将变为活动状态
,并向该用户发送确认电子邮件,
并将条目添加到系统日志
(等)。
这种情况对于查看单个命令如何影响基础结构的不同部分很有用,在这种情况下,您的数据库(某种“活动”标志),邮件服务器,系统日志等会受到影响。
这样的场景也确实可以帮助您设置测试驱动开发环境。
最后,思考命令确实可以帮助您创建面向任务的应用程序。您的用户将对此表示赞赏:-)
表达命令
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表单。我更喜欢这种方法,因为它结合了多个紧密相关的方面:
- 命令的执行(它做什么?)
- 验证命令参数(可以执行此操作吗?)
- 命令演示(如何执行此操作?)
表格
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
<h1>Welcome, {{ user|friendly_name }}</h1>
template_tags.py
@register.filter
def friendly_name(user):
return remote_api.get_cached_name(user.id)
查询方法
如果您的查询不只是表示形式的查询,则可以将查询添加到您的services.py(如果正在使用的话),或者引入querys.py模块:
querys.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.get_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)
第一种选择是在命令中更新这些模型。如果仅通过一个或两个命令更改这些模型,这将非常有用。
表格
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或类似框架将信号处理任务转移给后台任务。
signal.py
user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])
表格
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()
保持清洁
使用这种方法时,很容易确定代码是否保持干净。只需遵循以下准则:
- 我的模型中是否包含比管理数据库状态还执行更多功能的方法?您应该提取命令。
- 我的模型是否包含未映射到数据库字段的属性?您应该提取一个查询。
- 我的模型是否引用了不是数据库的基础架构(例如邮件)?您应该提取命令。
视图也一样(因为视图经常遇到相同的问题)。
- 我的视图是否主动管理数据库模型?您应该提取命令。
一些参考
回答 1
我通常在视图和模型之间实现服务层。这就像您项目的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()
请注意,我通常将模型,视图和服务带到模块级别,并根据项目的规模进一步分开
回答 2
首先,不要重复自己。
然后,请注意不要过度设计,有时这只是浪费时间,并使某人失去对重要内容的关注。回顾python的禅宗不时。
看一下活跃的项目
- 更多的人=更多需要适当组织
- 在Django的存储库,他们有一个简单的结构。
- 在点子库,他们有一个straigtforward目录结构。
该面料库也是一个很好的来看待。
- 您可以将所有模型放置在
yourapp/models/logicalgroup.py
- 您可以将所有模型放置在
- 例如
User
,Group
相关模型可能会失败yourapp/models/users.py
- 例如
Poll
,Question
,Answer
…可以去下yourapp/models/polls.py
- 加载您需要的内容
__all__
里面东西yourapp/models/__init__.py
- 模型就是你的数据
- 这包括您的实际数据
- 这还包括您的会话/ Cookie /缓存/ FS /索引数据
- 用户与控制器交互以操纵模型
- 这可以是API,也可以是保存/更新数据的视图
- 可以通过
request.GET
/ 进行调整request.POST
… etc - 也考虑分页或过滤。
- 数据更新视图
- 模板获取数据并相应地格式化
- 甚至没有模板的API都是视图的一部分;例如
tastypie
或piston
- 这也应该考虑中间件。
- 如果您需要为每个请求完成一些工作,那么中间件是一种解决方法。
- 例如添加时间戳
- 例如,更新有关网页点击量的指标
- 例如,填充缓存
- 如果您的代码片段总是在格式化对象时反复出现,那么模板标签就不错了。
- 例如,活动选项卡/ URL面包屑
利用模型经理
- 创建
User
可以进入UserManager(models.Manager)
。 - 实例的血腥细节应该在上面
models.Model
。 - 有关的细节,
queryset
可以进去models.Manager
。 - 您可能想一次创建一个对象
User
,因此您可能认为它应该存在于模型本身,但是在创建对象时,您可能并没有所有的细节:
例:
class UserManager(models.Manager):
def create_user(self, username, ...):
# plain create
def create_superuser(self, username, ...):
# may set is_superuser field.
def activate(self, username):
# may use save() and send_mail()
def activate_in_bulk(self, queryset):
# may use queryset.update() instead of save()
# may use send_mass_mail() instead of send_mail()
尽可能使用表格
如果您有映射到模型的表单,则可以省去很多样板代码。的ModelForm documentation
还不错。如果您有很多自定义功能,则最好将表单的代码与模型代码分开(或者为避免更高级的使用有时避免循环导入错误)。
尽可能使用管理命令
- 例如
yourapp/management/commands/createsuperuser.py
- 例如
yourapp/management/commands/activateinbulk.py
如果您有业务逻辑,可以将其分离出来
django.contrib.auth
使用后端,就像db有一个后端…等。setting
为您的业务逻辑添加一个(例如AUTHENTICATION_BACKENDS
)- 你可以用
django.contrib.auth.backends.RemoteUserBackend
- 你可以用
yourapp.backends.remote_api.RemoteUserBackend
- 你可以用
yourapp.backends.memcached.RemoteUserBackend
- 将困难的业务逻辑委托给后端
- 确保在输入/输出上设置期望值。
- 更改业务逻辑就像更改设置一样简单:)
后端示例:
class User(db.Models):
def get_present_name(self):
# property became not deterministic in terms of database
# data is taken from another service by api
return remote_api.request_user_name(self.uid) or 'Anonymous'
可能成为:
class User(db.Models):
def get_present_name(self):
for backend in get_backends():
try:
return backend.get_present_name(self)
except: # make pylint happy.
pass
return None
有关设计模式的更多信息
有关界面边界的更多信息
- 您要使用的代码确实是模型的一部分吗?->
yourapp.models
- 代码是业务逻辑的一部分吗?->
yourapp.vendor
- 代码是通用工具/库的一部分吗?->
yourapp.libs
- 代码是业务逻辑库的一部分吗?->
yourapp.libs.vendor
或yourapp.vendor.libs
- 这是一个很好的例子:您可以独立测试代码吗?
- 对很好 :)
- 不,您可能有接口问题
- 当有明确的分离时,使用嘲笑可以使单元测试变得轻而易举
- 分离符合逻辑吗?
- 对很好 :)
- 不,您可能无法单独测试这些逻辑概念。
- 您认为当您获得10倍以上的代码时是否需要重构?
- 是的,没有好处,没有布宜诺斯艾利斯,重构可能需要大量工作
- 不,那太棒了!
简而言之,您可以
yourapp/core/backends.py
yourapp/core/models/__init__.py
yourapp/core/models/users.py
yourapp/core/models/questions.py
yourapp/core/backends.py
yourapp/core/forms.py
yourapp/core/handlers.py
yourapp/core/management/commands/__init__.py
yourapp/core/management/commands/closepolls.py
yourapp/core/management/commands/removeduplicates.py
yourapp/core/middleware.py
yourapp/core/signals.py
yourapp/core/templatetags/__init__.py
yourapp/core/templatetags/polls_extras.py
yourapp/core/views/__init__.py
yourapp/core/views/users.py
yourapp/core/views/questions.py
yourapp/core/signals.py
yourapp/lib/utils.py
yourapp/lib/textanalysis.py
yourapp/lib/ratings.py
yourapp/vendor/backends.py
yourapp/vendor/morebusinesslogic.py
yourapp/vendor/handlers.py
yourapp/vendor/middleware.py
yourapp/vendor/signals.py
yourapp/tests/test_polls.py
yourapp/tests/test_questions.py
yourapp/tests/test_duplicates.py
yourapp/tests/test_ratings.py
或任何其他可以帮助您的东西;找到所需的接口和边界将对您有所帮助。
回答 3
Django使用了一种稍微修改的MVC。Django中没有“控制器”的概念。最接近的代理是“视图”,它倾向于与MVC转换混淆,因为在MVC中,视图更像Django的“模板”。
在Django中,“模型”不仅是数据库抽象。在某些方面,它与Django作为MVC的控制器的“视图”共同承担责任。它包含与实例相关联的全部行为。如果该实例需要与外部API交互作为其行为的一部分,那么那仍然是模型代码。实际上,根本不需要模型与数据库进行交互,因此可以想象,模型完全作为外部API的交互层存在。它是“模型”的更自由的概念。
回答 4
正如Chris Pratt所说,在Django中,MVC结构不同于其他框架中使用的经典MVC模型,我认为这样做的主要原因是避免过于严格的应用程序结构,就像在其他MVC框架(如CakePHP)中那样。
在Django中,MVC是通过以下方式实现的:
视图层分为两部分。该视图仅应用于管理HTTP请求,它们将被调用并对其进行响应。视图与应用程序的其余部分(表单,模型表单,自定义类,在简单情况下直接与模型)进行通信。要创建界面,我们使用模板。模板就像Django的字符串一样,它将一个上下文映射到其中,并且该上下文由应用程序传达给视图(当视图询问时)。
模型层提供封装,抽象,验证,智能,并使您的数据面向对象(他们说有朝一日DBMS也将面向对象)。这并不意味着您应该制作巨大的models.py文件(实际上,一个很好的建议是将模型分成不同的文件,将它们放入名为“ models”的文件夹中,在其中创建一个“ __init__.py”文件导入所有模型并最终使用models.Model类的属性“ app_label”的文件夹)。模型应该使您摆脱对数据的操作,这将使您的应用程序更简单。如果需要,您还应该为模型创建外部类,例如“工具”。您还可以在模型中使用继承,将模型的Meta类的“抽象”属性设置为“真”。
其余在哪里?好吧,小型Web应用程序通常是数据的一种接口,在某些小型程序中,使用视图查询或插入数据就足够了。更常见的情况是使用Forms或ModelForms,它们实际上是“控制器”。这不是解决一个常见问题的实用方法,而且是非常快速的方法。这就是网站要做的事情。
如果Forms不适合您,那么您应该创建自己的类来解决问题,一个很好的例子是管理应用程序:您可以阅读ModelAmin代码,它实际上可以用作控制器。没有标准的结构,我建议您检查现有的Django应用程序,具体取决于每种情况。这就是Django开发人员的意图,您可以添加xml解析器类,API连接器类,添加Celery来执行任务,为基于反应堆的应用程序而扭曲,仅使用ORM,制作Web服务,修改管理应用程序等等。 ..您有责任制作高质量的代码,无论是否尊重MVC哲学,使其基于模块并创建自己的抽象层。非常灵活。
我的建议是:尽可能多地阅读代码,周围有很多django应用程序,但是不要那么认真地对待它们。每种情况都是不同的,模式和理论会有所帮助,但并非总是如此,这不是很精确,django只是为您提供了一些有用的工具,您可以使用这些工具来减轻一些麻烦(例如管理界面,Web表单验证,i18n,观察者模式实施,所有以及之前提到的内容和其他内容),但是好的设计来自经验丰富的设计师。
PS .:使用auth应用程序中的“ User”类(来自标准django),您可以创建用户个人资料,或者至少读取其代码,这对您的情况很有用。
回答 5
一个古老的问题,但是我还是想提供我的解决方案。基于接受,模型对象也需要一些其他功能,而将它们放置在models.py中很尴尬。可以根据个人喜好单独编写繁琐的业务逻辑,但是我至少喜欢该模型来完成与自身相关的所有事情。该解决方案还为那些喜欢将所有逻辑放在模型中的人提供支持。
因此,我设计了一种hack,使我可以将逻辑与模型定义分开,并且仍然可以从IDE中获得所有提示。
优点应该很明显,但这列出了我观察到的一些优点:
- 数据库定义仅保留了这一点-没有附加逻辑“垃圾”
- 与模型相关的逻辑都整齐地放在一个地方
- 所有服务(表单,REST,视图)都具有单个逻辑访问点
- 最棒的是:一旦意识到我的models.py变得过于混乱并且不必将逻辑分开,就不必重写任何代码。分离是平滑且迭代的:我可以一次执行一个函数,也可以一次执行整个类或整个model.py。
我一直在Python 3.4和更高版本以及Django 1.8和更高版本上使用它。
app / models.py
....
from app.logic.user import UserLogic
class User(models.Model, UserLogic):
field1 = models.AnyField(....)
... field definitions ...
app / logic / user.py
if False:
# This allows the IDE to know about the User model and its member fields
from main.models import User
class UserLogic(object):
def logic_function(self: 'User'):
... code with hinting working normally ...
我唯一不知道的是如何使我的IDE(在本例中为PyCharm)识别UserLogic实际上是用户模型。但是由于这显然是黑客,所以我很高兴接受总是为self
参数指定类型的小小的麻烦。
回答 6
我必须同意你的看法。Django有很多可能性,但是最好的起点是回顾Django的设计理念。
从模型属性调用API是不理想的,似乎在视图中执行这样的事情并可能创建一个服务层来保持干燥是更有意义的。如果对API的调用是非阻塞的,并且调用成本很高,则可以将请求发送给服务工作者(从队列中消耗资源的工作者)。
按照Django的设计理念,模型封装了“对象”的各个方面。因此,与该对象相关的所有业务逻辑都应存在于此:
包括所有相关领域逻辑
模型应遵循Martin Fowler的Active Record设计模式来封装“对象”的各个方面。
您描述的副作用是显而易见的,这里的逻辑可以更好地分解为Querysets和manager。这是一个例子:
models.py
import datetime from djongo import models from django.db.models.query import QuerySet from django.contrib import admin from django.db import transaction class MyUser(models.Model): present_name = models.TextField(null=False, blank=True) status = models.TextField(null=False, blank=True) last_active = models.DateTimeField(auto_now=True, editable=False) # As mentioned you could put this in a template tag to pull it # from cache there. Depending on how it is used, it could be # retrieved from within the admin view or from a custom view # if that is the only place you will use it. #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' # Moved to admin as an action # def activate(self): # # method now has a side effect (send message to user) # self.status = 'activated' # self.save() # # send email via email service # #send_mail('Your account is activated!', '…', [self.email]) class Meta: ordering = ['-id'] # Needed for DRF pagination def __unicode__(self): return '{}'.format(self.pk) class MyUserRegistrationQuerySet(QuerySet): def for_inactive_users(self): new_date = datetime.datetime.now() - datetime.timedelta(days=3*365) # 3 Years ago return self.filter(last_active__lte=new_date.year) def by_user_id(self, user_ids): return self.filter(id__in=user_ids) class MyUserRegistrationManager(models.Manager): def get_query_set(self): return MyUserRegistrationQuerySet(self.model, using=self._db) def with_no_activity(self): return self.get_query_set().for_inactive_users()
管理员
# Then in model admin class MyUserRegistrationAdmin(admin.ModelAdmin): actions = ( 'send_welcome_emails', ) def send_activate_emails(self, request, queryset): rows_affected = 0 for obj in queryset: with transaction.commit_on_success(): # send_email('welcome_email', request, obj) # send email via email service obj.status = 'activated' obj.save() rows_affected += 1 self.message_user(request, 'sent %d' % rows_affected) admin.site.register(MyUser, MyUserRegistrationAdmin)
回答 7
我大多同意选择的答案(https://stackoverflow.com/a/12857584/871392),但想在“进行查询”部分中添加选项。
可以为模型定义QuerySet类,以进行过滤器查询等。之后,您可以将此查询集类代理给模型的管理器,就像内置管理器和QuerySet类一样。
虽然,如果必须查询多个数据模型以获得一个域模型,对我来说,将其放在像以前建议的那样的单独模块中似乎更合理。
回答 8
关于优缺点的不同选择的最全面的文章:
- 想法1:胖模型
- 想法2:将业务逻辑放入视图/表单
- 理念3:服务
- 理念4:QuerySet / Manager
- 结论
资料来源:https : //sunscrapers.com/blog/where-to-put-business-logic-django/
回答 9
Django设计用于轻松交付网页。如果您对此不满意,则应该使用其他解决方案。
我在模型上写根或通用操作(具有相同的接口),在模型控制器上写其他操作。如果需要其他模型的操作,请导入其控制器。
这种方法对我和应用程序的复杂性已经足够。
Hedde的回应是一个示例,展示了django和python本身的灵活性。
无论如何,这是一个非常有趣的问题!