在Django的业务逻辑和数据访问的分离(Separation of business logic

2019-06-18 07:09发布

我写在Django一个项目,我看到代码的80%是在文件models.py 。 此代码是混乱的,并经过一定时间后,我不再明白什么是真正发生的事情。

这里是让我困扰:

  1. 我觉得它很丑,我的模型层(这应该是只针对从数据库中的数据的工作负责)也发送邮件,走在API的其他服务等。
  2. 另外,我看不顺眼的地方商业逻辑视图,因为这样一来就变得难以控制。 举例来说,在我的应用程序至少有三种方式来创建新实例User ,但在技术上应均匀创建它们。
  3. 我并不总是注意到,当我的模型的方法和属性变得不确定性,当他们开发的副作用。

下面是一个简单的例子。 起初, 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应用这种方法的良好做法?

Answer 1:

好像你是问有关数据模型领域模型之间的区别-后者是在那里你可以找到业务逻辑和实体您的最终用户感知,前者是你实际存储的数据。

此外,我理解为你的问题的第三部分:如何发现故障,以保持这些模型分开。

这是两个截然不同的概念,它总是很难让他们分开。 不过,也有可以被用于此目的的一些共同的模式和工具。

关于域模型

你需要认识到的第一件事是,你的领域模型是不是真的有关数据; 它是关于行动问题,比如“激活此用户”,“取消该用户”,“哪些用户正在激活?”,以及“这是什么用户名?”。 在古典方面:它是关于查询命令

在命令思考

让我们先看看在你的榜样的命令启动:“启用此用户”和“取消该用户”。 有关命令的好处是,他们可以很容易地给小-时,则场景的表示:

不活动的用户
管理员激活该用户
然后该用户变为活动
确认电子邮件被发送到用户
一个条目添加到系统日志
(等等,等等)

这种情况的是怎么看你的基础架构的不同部分可以通过一个单一的命令影响有用的 - 在这种情况下,你的数据库(某种“活跃”标志的),你的邮件服务器,你的系统日志,等等。

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

最后,在命令以为真的可以帮助您创建一个面向任务的应用程序。 你的用户会明白这一点:-)

表达命令

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最基本的查询使用管理的对象:

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的基本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)

第一个选项是在你的命令来更新这些模型。 这是如果这些模型仅由一个或两个命令改变是非常有用的。

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文档:信号

建筑:领域驱动设计



Answer 2:

我通常在视图和模型之间实现服务层。 这就像你的项目的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 """
    if limit:
        return Book.objects.filter(**filters)[:limit]
    return Book.objects.filter(**filters)

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

你要知道,我通常采取模型,视图和服务,模块级,甚至进一步分离根据项目的大小



Answer 3:

首先, 不要重复自己 。

然后,请注意不要overengineer,有时它只是在浪费时间,并且使别人失去专注于什么是重要的。 查看蟒蛇的禅宗不时。

看看活动项目

  • 越来越多的人=更需要妥善安排
  • 在Django的仓库 ,他们有一个简单的结构。
  • 在点子库 ,他们有一个straigtforward目录结构。
  • 该面料库也是一个很好的来看待。

    • 您可以将您的下所有型号yourapp/models/logicalgroup.py
  • UserGroup和相关的模型可以去下yourapp/models/users.py
  • 例如PollQuestionAnswer ...下可以去yourapp/models/polls.py
  • 加载你需要什么__all__yourapp/models/__init__.py

更多关于MVC

  • 模型数据
    • 这包括实际数据
    • 这也包括您的会议/的Cookie /缓存/ FS /索引数据
  • 用户用控制器来操纵模型交互
    • 这可能是一个API,或者保存视图/更新数据
    • 这可以被调整request.GET / request.POST ...等
    • 认为传呼过滤过。
  • 数据更新视图
    • 模板取数据,并相应地将其格式化
    • 的API,即使W / O型模板是视图的一部分; 例如tastypiepiston
    • 这也应该占中间件。

利用中间件 / templatetags

  • 如果你需要一些工作,为每个请求做,中间件是一条路可走。
    • 如添加时间戳
    • 有关页面点击更新如指标
    • 如填充缓存
  • 如果你的代码,总是重复出现的格式化对象片断,templatetags都不错。
    • 例如,有源标签/ 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

更多关于设计模式

  • 有已经有大约一个很好的问题设计模式
  • 一个非常约实用的设计模式,良好的视频
  • Django的后端是显而易见的授权使用设计模式。

更多关于接口的边界

  • 是您要使用的机型还真部分的代码? - > yourapp.models
  • 是业务逻辑代码的一部分? - > yourapp.vendor
  • 是通用的工具/库的代码的一部分? - > yourapp.libs
  • 是业务逻辑库的代码的一部分? - > yourapp.libs.vendoryourapp.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

或其他任何可以帮助你; 找到接口,你需要界限会帮助你。



Answer 4:

Django的采用了略微修改一种MVC的。 有没有在Django“控制”的概念。 最近代理是一个“视图”,这往往会导致混乱MVC转换,因为在MVC视图更像Django的“模板”。

在Django中,一个“模式”不仅是一个数据库抽象。 在某些方面,这股与Django的“观点”为MVC的控制器职责。 它拥有与实例相关联的行为的全部。 如果实例需要与外部交互的API作为其一部分的行为,那么这仍然是型号代码。 事实上,不要求模特在所有与数据库进行交互,所以你能想到的有完全存在作为一个互动层外部API模型。 这是一个“模式”的一个更自由的概念。



Answer 5:

在Django中,MVC结构是克里斯·普拉特说,从其他框架中使用的经典的MVC模式的不同,我觉得这样做的主要原因是避免过于严格的应用程序结构,就像发生在其他MVC框架一样的CakePHP。

在Django中,MVC中的下列方式实现:

View层在两个分裂。 该意见应只用于管理HTTP请求,他们被称为,并作出回应。 查看与应用程序的其余部分(形式,modelforms,定制类的简单的情况下直接与模型)进行通信。 要创建我们使用模板的接口。 模板是绳状在模块中使用,它的上下文映射到它们,这方面是由应用程序传送到视图(当视图问)。

模型层提供了封装,抽象,确认,智力,让您的数据面向对象(他们说哪天DBMS也将)。 这并不意味着你应该让巨大的models.py文件(实际上是一个很好的建议是在不同的文件分割你的模型,把它们放入一个叫做“模型”文件夹,使一个“__init__.py”文件到本在那里你导入所有的模型和最后使用models.Model类的属性“app_label”文件夹)。 模型应抽象您从数据操作,它会使你的应用程序更简单。 你应该还,如果需要的话,创建外部类,如“工具”为您models.You也可以使用传统的模型,设定模型的元级的为“True”的“抽象”的属性。

在哪里休息吗? 那么,小型Web应用程序通常是一种数据的接口,在使用视图查询或插入数据就足够了一些小程序的案件。 更常见的情况下,将使用窗体或ModelForms,这实际上是“控制器”。 这不是比一个实际的解决方案,以一个共同的问题,和一个非常快的一个等。 这是一个网站用来做什么。

如果表格没有enogh你,那么你应该创建自己的类来完成的魔力,这一个很好的例子是管理应用程序:你可以阅读ModelAmin代码,这实际上可以作为一个控制器。 没有一个标准的结构,我建议你检查现有的Django的应用程序,这取决于每个案件。 这是Django的开发意图,你可以添加XML解析器类的API连接器类,加入芹菜来执行任务,扭曲了基于反应器的应用程序,只使用ORM,使Web服务,修改管理应用程序和更多。 ..这是你的责任,使质量好的代码,尊重MVC理念与否,使基础和创建自己的抽象层IT课程。 这是非常灵活的。

我的建议是:多读代码,你可以有很多的Django应用程序周围,但不要把他们那么认真。 每个案件都是不同的,模式和理论帮助,但并非总是如此,这是一个不精确的cience,Django的只是提供你,你可以用它来aliviate一些疼痛(如管理员界面,网页表单验证,国际化,观察者模式实现很好的工具,所有的前面提到的和其他人),但良好的设计来自经验丰富的设计师。

PS:从身份验证应用程序(从标准的Django)使用“用户”类,可以使例如用户配置文件,或至少读它的代码,这将是你的情况非常有用。



Answer 6:

我主要与选择的答案(同意https://stackoverflow.com/a/12857584/871392 ),但要在如何查询部分添加选项。

一个可以定义模型上要将过滤器的查询和子查询集类。 之后,你可以代理这个查询集类模型的管理器,例如内置的管理和查询集类的事情。

虽然,如果你要查询一些数据模型,以获得一个域模型,它似乎更合理的,我像以前一样建议把这个单独的模块。



Answer 7:

我会同意你的看法。 有很多的Django的可能性,但启动正在审查最好的地方Django的设计理念 。

  1. 调用从模型属性的API不是很理想,现在看来似乎会更有意义,做这样的事情的看法,并可能创建一个服务层,以保持干燥的事情。 如果调用API是无阻塞的调用是一个昂贵的一个,发送请求到服务工作者可能是有意义的(即从队列中消耗了工人)。

  2. 按照Django的设计理念模型封装“对象”的每一个方面。 因此,与该对象的所有业务逻辑应该生活在那里:

包括所有相关的领域逻辑

模型应该封装的各个方面的“对象”,以下Martin Fowler的Active Record的设计模式。

  1. 副作用你的描述是显而易见的,这里的逻辑,可以更好地分解成查询集和管理人员。 下面是一个例子:

    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) 


Answer 8:

一个老问题,但我想反正我提供解决方案。 它是基于接受该模型对象也需要一些额外的功能,同时它的尴尬将它放置在models.py内。 重商业逻辑可以分别视个人口味来写的,但我至少喜欢做模型与自己的一切。 该解决方案还支持那些谁喜欢都放在模型本身中的逻辑。

因此,我设计了一个黑客 ,让我从模型定义中分离逻辑,并且仍然从我的IDE得到所有的提示。

优点应该是显而易见的,但是这列出了我观察到的几个:

  • DB定义仍然只是 - 没有逻辑“垃圾”附着
  • 模型相关的逻辑都整齐地摆放在一个地方
  • 所有服务(形式,REST,景色)有一个单一访问点的逻辑
  • 最重要的是:我没有重写任何代码,一旦我意识到,我的models.py变得太混乱,不得不在逻辑中分离出来了。 分离是顺利和迭代:我可以在同一时间或整个班级或整个models.py做一个功能。

我一直在使用这与Python 3.4和更大的和Django的1.8和更大。

应用程序/ 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实际上是用户模型。 但是,由于这显然是一个黑客,我很乐意接受的始终指定类型的小滋扰self参数。



Answer 9:

Django的设计为easely用来提供网页。 如果你不舒适与这个也许你应该用另一种解决方案。

我书面方式模型上的根目录或共同操作(具有相同的接口)和模型的控制器上的其他人。 如果我需要从其他模型中的操作我导入它的控制器。

这种方法是足以让我和我的应用程序的复杂性。

Hedde的反应是表示Django和Python本身的灵活性的示例。

非常有趣的问题呢!



文章来源: Separation of business logic and data access in django