django中业务逻辑和数据访问的分离

我正在用Django写一个项目,我看到80%的代码都在文件models.py中。这段代码令人困惑,过了一段时间后,我就不明白到底发生了什么。

这是困扰我的问题:

    我发现它丑陋,我的模型水平(这应该是 仅负责处理来自数据库的数据)也是 此外,我发现在视图中放置业务逻辑是不可接受的,因为 这样就很难控制了。例如,在我的 应用程序至少有三种创建新方法 . User的实例,但技术上应该统一创建它们
  1. 我不总是注意当方法和 我的模型的属性变得不确定,当它们发展时 李副作用。< / >

这里有一个简单的例子。一开始,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])

我想要的是在代码中分离实体:

  1. 数据库级实体,即数据库级逻辑:我的应用程序存储什么样的数据?
  2. 应用程序级实体,即业务级逻辑:我的应用程序做什么?

在Django中实现这样一种方法有哪些好的实践呢?

142933 次浏览

我通常在视图和模型之间实现一个服务层。这就像你的项目的API,给你一个良好的直升机视图正在发生什么。我从我的一个同事那里继承了这个实践,他在Java项目(JSF)中经常使用这种分层技术,例如:

# EYZ0

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()
请注意,我通常把模型、视图和服务放在模块级和 根据项目的大小进一步分开

Django使用了一种稍微修改过的MVC。在Django中没有“控制器”的概念。最接近的代理是一个“视图”,它容易引起MVC转换的混淆,因为在MVC中,视图更像是Django的“模板”。

在Django中,“模型”不仅仅是一个数据库抽象。在某些方面,它与Django的“视图”分担了MVC控制器的职责。它保存了与实例关联的全部行为。如果该实例需要与外部API交互作为其行为的一部分,那么这仍然是模型代码。事实上,模型根本不需要与数据库交互,因此您可以想象到模型完全作为外部API的交互层存在。这是一个更自由的“模型”概念。

首先,不要重复自己的话

然后,请注意不要过度设计,有时只是浪费时间,让人失去对重要事情的关注。不时回顾蟒之禅

看一看活跃的项目

  • 人越多=组织得越好
  • django库他们有一个简单的结构。
  • 皮普库,它们有一个简单的目录结构。
  • 布库也是一个很好的例子。

    • 你可以把你所有的模型放在yourapp/models/logicalgroup.py
    • 李< / ul > < / >
    • 例如UserGroup和相关型号可以放在yourapp/models/users.py
    • 例:PollQuestionAnswer…可以放在yourapp/models/polls.py
    • 加载你在__all__中需要的东西到yourapp/models/__init__.py

    更多关于MVC的信息

    • 模型是你的数据
      • 这包括您的实际数据
      • 这也包括你的session / cookie / cache / fs / index数据
      • 李< / ul > < / >
      • 用户与控制器交互操作模型
        • 这可能是一个API,或者一个保存/更新数据的视图
        • 这可以用request.GET / request.POST等来调整
        • 也想想分页过滤
        • 李< / ul > < / >
        • 数据更新视图
          • 模板获取数据并相应地格式化它
          • api甚至w/o模板也是视图的一部分;例:tastypiepiston
          • 这也应该考虑到中间件。
          • 李< / ul > < / >

          利用中间件 / templatetags

            如果你需要为每个请求做一些工作,中间件是一种方法。
            • 例如,添加时间戳
            • 例如,更新页面点击率的指标
            • 例如,填充缓存
            • 李< / ul > < / > 如果你的代码片段总是在格式化对象时重复出现,templatetags是很好的选择。
              • 例如,active TAB / url面包屑
              • 李< / ul > < / >

              利用模型管理器

              • 创建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()
              

              尽可能使用表单

              如果您有映射到模型的表单,那么可以省去大量的样板代码。EYZ1相当不错。如果您有很多自定义(或者有时为了更高级的用途避免循环导入错误),那么将表单代码与模型代码分离是很好的。

              尽可能使用管理命令

              • 例如:# EYZ0
              • 例如:# EYZ0

              如果您有业务逻辑,您可以将其分离出来

              • 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
              

              关于设计模式的更多信息

              关于接口边界的更多信息

              • 您想要使用的代码真的是模型的一部分吗?- > # EYZ0
              • 代码是业务逻辑的一部分吗?- > # EYZ0
              • 代码是通用工具/库的一部分吗?- > # EYZ0
              • 代码是否是业务逻辑库的一部分?-> yourapp.libs.vendoryourapp.vendor.libs
              • 这里有一个很好的例子:你能独立测试你的代码吗?
                • 是的,很好:)
                • 不,你可能有接口问题
                • 当有明确的分离时,使用嘲笑的使用的unittest应该是轻而易举的
                • 李< / ul > < / >
                • 分离合乎逻辑吗?
                  • 是的,很好:)
                  • 不,单独测试这些逻辑概念可能会有困难。
                  • 李< / ul > < / > 当你得到10倍多的代码时,你认为你需要重构吗?
                    • 是的,不好,不好,重构可能会有很多工作
                    • 不,那太棒了!
                    • 李< / ul > < / >

                    简而言之,你本可以

                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0
                    • # EYZ0

                    或者其他对你有帮助的东西;找到您需要的接口边界会对你有帮助。

似乎你在问数据模型域模型之间的区别——后者是你可以找到最终用户感知到的业务逻辑和实体的地方,前者是你实际存储数据的地方。

此外,我将你问题的第三部分解释为:如何注意到未能将这些模型分开。

这是两个完全不同的概念,很难把它们分开。但是,有一些常见的模式和工具可用于此目的。

关于领域模型

您需要认识到的第一件事是您的领域模型实际上与数据无关;它是关于行动问题的,例如“激活这个用户”,“禁用这个用户”,“哪些用户当前被激活”,以及“这个用户的名字是什么”。用经典的术语来说:这是关于查询命令

命令思维

让我们从示例中的命令开始:以及“禁用此用户”。命令的好处是它们可以很容易地用小的given-when-then场景来表示:

鉴于一个不活跃的用户
管理员激活这个用户
然后用户变为活动
而且向用户
发送确认电子邮件 而且添加到系统日志
(等等。)< / p >

这样的场景有助于了解单个命令如何影响基础设施的不同部分——在这种情况下,您的数据库(某种“活动”标志)、邮件服务器、系统日志等。

这样的场景也能真正帮助你建立一个测试驱动开发环境。

最后,用命令思考确实有助于创建面向任务的应用程序。你的用户会喜欢的:-)

表达命令

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 Form。我更喜欢这种方法,因为它结合了多个密切相关的方面:

  • 命令的执行(它做什么?)
  • 命令参数的验证(它能做到吗?)
  • 命令的表示(我如何做到这一点?)

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

<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(如果你正在使用它),或引入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.get_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()

更好的选择是使用自定义信号。这些信号当然是由您的命令发出的。信号的优点是可以使多个查询模型与原始模型保持同步。此外,信号处理可以使用芹菜或类似的框架卸载到后台任务。

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()
    

保持清洁

当使用这种方法时,确定代码是否保持干净变得非常容易。只要遵循这些指导方针:

  • 我的模型是否包含除了管理数据库状态以外的其他方法?您应该提取一个命令。
  • 我的模型是否包含不映射到数据库字段的属性?您应该提取一个查询。
  • 我的模型是否引用了不是数据库的基础设施(比如邮件)?您应该提取一个命令。

视图也是如此(因为视图经常遭受同样的问题)。

  • 视图是否主动管理数据库模型?您应该提取一个命令。

一些参考

Django文档:代理模型

Django文档:signals . bb0

架构:领域驱动的设计

在Django中,MVC结构正如Chris Pratt所说,与其他框架中使用的经典MVC模型不同,我认为这样做的主要原因是避免了过于严格的应用程序结构,就像在其他MVC框架(如CakePHP)中发生的那样。

在Django中,MVC是这样实现的:

视图层被分成两部分。视图应该只用于管理HTTP请求,它们被调用并响应它们。视图与应用程序的其余部分(表单、模型表单、自定义类,在简单情况下直接与模型通信)通信。 为了创建接口,我们使用模板。对于Django来说,模板是类似字符串的,它将一个上下文映射到模板中,这个上下文由应用程序传递给视图(当视图请求时)

模型层提供封装、抽象、验证、智能,并使您的数据成为面向对象的(他们说有一天DBMS也会这样)。这并不意味着你应该创建巨大的models.py文件(实际上一个非常好的建议是将你的模型分割到不同的文件中,将它们放入一个名为“models”的文件夹中,在这个文件夹中创建一个“__init__.py”文件,在这个文件夹中导入所有的模型,最后使用模型的属性“app_label”。模型类)。模型应该把你从数据操作中抽象出来,它将使你的应用程序更简单。如果需要,您还应该为您的模型创建外部类,例如“工具”。你也可以在模型中使用继承,将模型元类的“abstract”属性设置为“True”。

剩下的在哪里?小型web应用程序通常是一种数据接口,在一些小型程序中,使用视图来查询或插入数据就足够了。更常见的情况是使用form或modelform,它们实际上是“控制器”。这是一个常见问题的实际解决方案,而且是一个非常快速的解决方案。这就是网站的作用。

如果表单对您来说还不够,那么您应该创建自己的类来实现神奇的功能,一个很好的例子是管理应用程序:您可以阅读ModelAmin代码,这实际上是作为控制器工作的。没有一个标准的结构,我建议你检查现有的Django应用程序,这取决于每个情况。这就是Django开发人员想要的,你可以添加xml解析器类,API连接器类,添加用于执行任务的芹菜,用于基于reactor的应用程序,只使用ORM,创建web服务,修改管理应用程序等等……你有责任编写高质量的代码,尊重MVC理念,基于模块,创建你自己的抽象层。它非常灵活。

我的建议是:尽可能多地阅读代码,周围有很多django应用程序,但不要太把它们当回事。每个案例都是不同的,模式和理论会有所帮助,但并不总是如此,这是一门不精确的科学,django只是为你提供了一些好的工具,你可以用它们来减轻一些痛苦(比如管理界面、web表单验证、i18n、观察者模式实现,以及前面提到的所有这些),但是好的设计来自有经验的设计师。

PS.:使用'User'类从auth应用程序(从标准django),你可以创建示例用户配置文件,或至少阅读它的代码,这将对你的情况很有用。

我基本上同意所选的答案(https://stackoverflow.com/a/12857584/871392),但想在制作查询部分添加选项。

可以为make过滤器查询等模型定义QuerySet类。之后,您可以为模型的管理器代理这个queryset类,就像内置的manager和queryset类一样。

尽管如此,如果您必须查询多个数据模型才能得到一个领域模型,那么对我来说,像之前建议的那样将其放在单独的模块中似乎更合理。

这是一个老问题,但我还是想提出我的解决方案。这是基于这样一个事实:模型对象也需要一些额外的功能,而把它放在models.py中是很尴尬的。重业务逻辑可以根据个人喜好单独编写,但我至少喜欢模型做与自身相关的所有事情。该解决方案还支持那些喜欢将所有逻辑置于模型本身中的人。

因此,我设计了一个黑客,它允许我将逻辑从模型定义中分离出来,并且仍然可以从IDE中获得所有提示。

优点应该是显而易见的,但下面列出了我观察到的一些优点:

  • DB定义仍然是那样-没有逻辑“垃圾”附加
  • 与模型相关的逻辑都整齐地放在一个地方
  • 所有服务(表单、REST、视图)都有一个逻辑访问点
  • 当我意识到我的models.py变得太杂乱,不得不分离逻辑时,我不需要重写任何代码。分离是平滑和迭代的:我可以一次处理一个函数,也可以一次处理整个类或整个models.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 ...

应用程序/逻辑/ 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实际上是用户模型。但由于这显然是一种hack,我很乐意接受总是为self参数指定类型的小麻烦。

我不得不同意你的看法。django中有很多可能性,但是最好从Django的设计理念开始。

  1. 从模型属性调用API并不理想,似乎在视图中做这样的事情更有意义,并可能创建一个服务层来保持干燥。如果对API的调用是非阻塞的,并且调用的开销很大,那么将请求发送到service worker(从队列中消费的worker)可能是有意义的。

  2. 根据Django的设计理念,模型封装了“对象”的各个方面。因此,所有与该对象相关的业务逻辑都应该存在于此:

包括所有相关的领域逻辑

模型应该封装“对象”的每个方面,遵循Martin Fowler的活动记录设计模式。

  1. 你描述的副作用是明显的,这里的逻辑可以更好地分解为queryset和管理器。这里有一个例子:

    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()
    

    admin.py

    # 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)
    

最全面的关于不同选择的利弊的文章:

  1. 想法1:胖模特
  2. 想法#2:将业务逻辑放在视图/表单中
  3. 理念#3:服务
  4. 想法4:QuerySets/Managers
  5. 结论
< p >来源: # EYZ0 < / p >

HackSoftware发布了一个Django和Django rest框架的风格指南,我认为这很有帮助。

这里描述的Django Styleguide的核心内容可以总结如下:

在Django中,业务逻辑应该存在于:

  • 服务——主要负责向数据库写入内容的函数。
  • 选择器——主要负责从数据库中获取数据的函数。
  • 模型属性(有一些例外)。
  • Model clean方法用于额外的验证(有一些例外)。

在Django中,业务逻辑不应该存在于:

  • api和视图。
  • 序列化器和表单。
  • 形式标记。
  • 模型save方法。
  • 自定义管理器或查询集。
  • 信号。

模型属性vs选择器:

  • 如果属性跨越多个关系,它最好是一个选择器。
  • 如果属性是非平凡的&序列化时很容易导致N + 1查询问题,它最好是一个选择器。

Django Style Guide