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

def*_*fuz 441 python django model-view-controller business-logic-layer data-access-layer

我正在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()
Run Code Online (Sandbox Code Playgroud)

随着时间的推移,它变成了这样:

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])
Run Code Online (Sandbox Code Playgroud)

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

  1. 我的数据库,数据库级别的实体:什么包含我的应用程序?
  2. 我的应用程序的实体,业务逻辑级别:什么可以使我的应用程序?

实现可以在Django中应用的这种方法有哪些好的做法?

pub*_*her 581

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

此外,我已将您问题的第3部分解释为:如何注意未能将这些模型分开.

这是两个非常不同的概念,并且总是很难将它们分开.但是,有一些常用的模式和工具可用于此目的.

关于域模型

您需要认识到的第一件事是您的域模型并不是真正的数据; 它是关于诸如"激活此用户","停用此用户","当前哪些用户已被激活?"以及"该用户名称是什么?"等操作问题.用经典术语来说:它是关于查询命令的.

在命令中思考

让我们从查看示例中的命令开始:"激活此用户"和"停用此用户".关于命令的好处是它们很容易通过小的给定时间场景来表达:


管理员激活此用户时,如果给予非活动用户,
用户变为活动状态
,并向用户发送确认电子邮件,
并将一个条目添加到系统日志中
(等等)

这样的场景对于了解单个命令如何影响基础架构的不同部分(在这种情况下是您的数据库(某种"活动"标志),邮件服务器,系统日志等)非常有用.

这样的场景也真正帮助您建立测试驱动开发环境.

最后,思考命令确实可以帮助您创建面向任务的应用程序.您的用户会很感激:-)

表达命令

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
Run Code Online (Sandbox Code Playgroud)

使用表格

另一种方法是为每个命令使用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
Run Code Online (Sandbox Code Playgroud)

在查询中思考

您的示例不包含任何查询,因此我冒昧地编写了一些有用的查询.我更喜欢使用术语"问题",但查询是经典术语.有趣的查询是:"此用户的名称是什么?","此用户可以登录吗?","显示已停用的用户列表"和"已停用用户的地理分布是什么?"

在着手回答这些问题之前,您应该总是问自己两个问题:这是一个仅针对我的模板的表示性查询,和/或与执行我的命令和/或报告查询相关的业务逻辑查询.

仅仅是为了改进用户界面而进行表示性查询.业务逻辑查询的答案直接影响命令的执行.报告查询仅用于分析目的,并且具有较宽松的时间限制.这些类别并不相互排斥.

另一个问题是:"我能完全控制答案吗?" 例如,在查询用户名时(在此上下文中),我们无法控制结果,因为我们依赖于外部API.

制作查询

Django中最基本的查询是使用Manager对象:

User.objects.filter(active=True)
Run Code Online (Sandbox Code Playgroud)

当然,这仅在数据实际表示在数据模型中时才有效.这并非总是如此.在这些情况下,您可以考虑以下选项.

自定义标签和过滤器

第一种替代方法对于仅仅是表示的查询非常有用:自定义标记和模板过滤器.

template.html

<h1>Welcome, {{ user|friendly_name }}</h1>
Run Code Online (Sandbox Code Playgroud)

template_tags.py

@register.filter
def friendly_name(user):
    return remote_api.get_cached_name(user.id)
Run Code Online (Sandbox Code Playgroud)

查询方法

如果您的查询不仅仅是表示性的,您可以向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 
Run Code Online (Sandbox Code Playgroud)

代理模型

代理模型在业务逻辑和报告的上下文中非常有用.您基本上定义了模型的增强子集.您可以通过覆盖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
Run Code Online (Sandbox Code Playgroud)

查询模型

对于本质上复杂但经常执行的查询,可能存在查询模型.查询模型是非规范化的一种形式,其中单个查询的相关数据存储在单独的模型中.当然,技巧是使非规范化模型与主模型保持同步.只有在完全由您控制的情况下才能使用查询模型.

models.py

class InactiveUserDistribution(models.Model):
    country = CharField(max_length=200)
    inactive_user_count = IntegerField(default=0)
Run Code Online (Sandbox Code Playgroud)

第一个选项是在命令中更新这些模型.如果仅通过一个或两个命令更改这些模型,这非常有用.

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()
Run Code Online (Sandbox Code Playgroud)

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

signals.py

user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])
Run Code Online (Sandbox Code Playgroud)

forms.py

class ActivateUserForm(forms.Form):
    # see above

    def execute(self):
        # see above
        user_activated.send_robust(sender=self, user=user)
Run Code Online (Sandbox Code Playgroud)

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()
Run Code Online (Sandbox Code Playgroud)

保持清洁

使用这种方法时,确定代码是否保持干净变得非常容易.请遵循以下准则:

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

视图也是如此(因为视图经常遇到同样的问题).

  • 我的观点是否主动管理数据库模型?你应该提取一个命令.

一些参考文献

Django文档:代理模型

Django文档:信号

架构:领域驱动设计

  • 很高兴看到将DDD纳入与django相关的问题的答案.仅仅因为Django使用ActiveRecord来保持持久性并不意味着关注点的分离应该会消失.很好的答案. (9认同)
  • 如果我想在删除该对象之前验证该被设置用户是对象的所有者,我应该在视图中还是在/ service模块中检查它? (6认同)
  • @Ivan:两个.它必须*在表单/服务模块中,因为它是您业务约束的一部分.它*应该*也在视图中,因为您应该只呈现用户可以实际执行的操作. (6认同)
  • 自定义管理器*方法*是实现查询的好方法:`User.objects.inactive_users()`.但是这里的代理模型示例IMO会导致错误的语义:`u = InactiveUser.objects.all()[0]; u.active = True; u.save()`和`isinstance(u,InactiveUser)== True`.另外我要提一下在很多情况下维护查询模型的有效方法是使用db视图. (4认同)
  • @adnanmuttaleb 这是正确的。请注意,答案本身仅使用术语“域模型”。我添加了 DDD 链接,并不是因为我的答案是 DDD,而是因为那本书在帮助您思考领域模型方面做得很好。 (2认同)

Hed*_*ide 135

我通常在视图和模型之间实现服务层.这就像您的项目的API一样,可以让您直观地了解正在发生的事情.我从我的同事那里继承了这种做法,这种做法在Java项目(JSF)中使用了这种分层技术,例如:

models.py

class Book:
   author = models.ForeignKey(User)
   title = models.CharField(max_length=125)

   class Meta:
       app_label = "library"
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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()
Run Code Online (Sandbox Code Playgroud)

请注意,我通常会将模型,视图和服务提供给模块级别,并根据项目的大小进一步分离

  • @arie不一定,也许是一个更好的例子,网店服务将包括诸如生成购物车会话,异步任务,如产品评级计算,创建和发送电子邮件等等. (8认同)
  • 我喜欢一般的方法,但根据我的理解,您的具体示例通常会实现为[manager](https://docs.djangoproject.com/en/dev/topics/db/managers/). (7认同)
  • 我也喜欢这种方法,也来自java.我是python的新手,你会如何测试views.py?你将如何模拟服务层(例如,如果服务进行一些远程api调用)? (4认同)

dno*_*zay 67

首先,不要重复自己.

然后,请注意不要过度工程,有时这只是浪费时间,并使某人失去对重要事项的关注.不时回顾蟒蛇禅宗.

看看活跃的项目

  • 更多人=更需要正确组织
  • Django的仓库,他们有一个简单的结构.
  • 点子库,他们有一个straigtforward目录结构.
  • 面料库也是一个很好的来看待.

    • 你可以放置所有模型 yourapp/models/logicalgroup.py
  • 例如User,Group相关模型可以进入yourapp/models/users.py
  • 例如Poll,Question,Answer...可以去下yourapp/models/polls.py
  • 加载你需要的__all__东西yourapp/models/__init__.py

更多关于MVC的信息

  • model是你的数据
    • 这包括您的实际数据
    • 这还包括您的session/cookie/cache/fs/index数据
  • 用户与控制器交互以操纵模型
    • 这可以是API,也可以是保存/更新数据的视图
    • 这可以用request.GET/ request.POST...等调整
    • 想想分页过滤.
  • 数据更新视图
    • 模板采用数据并相应地格式化
    • API甚至没有模板都是视图的一部分; 例如tastypiepiston
    • 这也应该占中间件.

利用中间件/模板标签

  • 如果您需要为每个请求完成一些工作,中间件是一种可行的方法.
    • 例如,添加时间戳
    • 例如,更新有关页面命中的指标
    • 例如填充缓存
  • 如果你有代码片段总是重复出现格式化对象,那么templatetags就是好的.
    • 例如活动标签/网址面包屑

利用模型经理

  • 创造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()
Run Code Online (Sandbox Code Playgroud)

尽可能使用表格

如果您有映射到模型的表单,则可以删除许多样板代码.该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' 
Run Code Online (Sandbox Code Playgroud)

可能成为:

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
Run Code Online (Sandbox Code Playgroud)

更多关于设计模式

更多关于界面边界

  • 您想要使用的代码是否真的是模型的一部分? - >yourapp.models
  • 代码是业务逻辑的一部分吗? - >yourapp.vendor
  • 代码是通用工具/库的一部分吗? - >yourapp.libs
  • 代码是业务逻辑库的一部分吗?- > yourapp.libs.vendoryourapp.vendor.libs
  • 这是一个很好的:你可以独立测试你的代码吗?
    • 对很好 :)
    • 不,你可能有接口问题
    • 当有明显的分离时,使用模拟来进行单元测试应该是轻而易举
  • 分离是否合乎逻辑?
    • 对很好 :)
    • 不,您可能无法单独测试这些逻辑概念.
  • 当你获得10倍以上的代码时,你认为你需要重构吗?
    • 是的,没有好,没有bueno,重构可能是很多工作
    • 不,那太棒了!

简而言之,你可以拥有

  • 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

或任何其他帮助你; 找到您需要接口边界将有助于您.


Chr*_*att 25

Django使用了一种稍微修改过的MVC.Django中没有"控制器"的概念.最接近的代理是"视图",这往往会导致与MVC转换混淆,因为在MVC中,视图更像是Django的"模板".

在Django中,"模型"不仅仅是数据库抽象.在某些方面,它与Django作为MVC控制者的"观点"共享义务.它包含与实例关联的整个行为.如果该实例需要与外部API交互作为其行为的一部分,那么这仍然是模型代码.事实上,模型根本不需要与数据库交互,因此您可以想象将模型完全作为外部API的交互层存在.这是一个更"自由"的"模型"概念.


Nat*_*ile 6

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

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

视图图层分为两部分.视图应仅用于管理HTTP请求,它们被调用并响应它们.视图与应用程序的其余部分(表单,模型,自定义类,简单情况下直接与模型)进行通信.要创建界面,我们使用模板.模板与Django类似,它将上下文映射到它们中,并且应用程序将此上下文传递给视图(当视图请求时).

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

其余的在哪里?好吧,小型Web应用程序通常是一种数据接口,在一些使用视图查询或插入数据的小程序案例就足够了.更常见的情况是使用Forms或ModelForms,它们实际上是"控制器".这不是解决常见问题的实际解决方案,而是一个非常快速的问题.这是一个网站用来做的事情.

如果Forms不适合你,那么你应该创建自己的类来实现魔术,这是一个很好的例子就是管理应用程序:你可以读取ModelAmin代码,这实际上是作为一个控制器.没有标准的结构,我建议你检查现有的Django应用程序,它取决于每种情况.这是Django开发人员的意图,您可以添加xml解析器类,API连接器类,添加Celery以执行任务,为基于反应器的应用程序加以扭曲,仅使用ORM,创建Web服务,修改管理应用程序等等. ..你有责任制作高质量的代码,尊重MVC理念与否,使其基于模块并创建自己的抽象层.它非常灵活.

我的建议:尽可能多地阅读代码,有很多django应用程序,但不要认真对待它们.每种情况都不同,模式和理论有所帮助,但并非总是如此,这是一种不精确的科学,django只是为您提供了很好的工具,可以用来解决一些痛苦(如管理界面,Web表单验证,i18n,观察者模式实现,所有前面提到的和其他的),但好的设计来自经验丰富的设计师.

PS.:使用auth应用程序中的'User'类(来自标准django),你可以制作例如用户配置文件,或者至少阅读它的代码,它对你的情况很有用.