django中业务逻辑和数据访问的分离 [英] Separation of business logic and data access in django

查看:37
本文介绍了django中业务逻辑和数据访问的分离的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

困扰我的是:

  1. 我觉得我的模型级别(本来应该是只负责处理来自数据库的数据)也是发送电子邮件、通过 API 访问其他服务等.
  2. 另外,我发现将业务逻辑放在视图中是不可接受的,因为这样就很难控制了.例如,在我的应用程序至少有三种方法可以创建新的User 的实例,但从技术上讲,它应该统一创建它们.
  3. 我并不总是注意到方法和我的模型的属性变得不确定,当它们发展时副作用.

这是一个简单的例子.起初,User 模型是这样的:

 类用户(db.Models):def get_present_name(self):返回 self.name 或匿名"定义激活(自我):self.status = '已激活'自我保存()

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

 类用户(db.Models):def get_present_name(self):# 属性在数据库方面变得不确定# 数据是由 api 从另一个服务中获取的返回 remote_api.request_user_name(self.uid) 或匿名"定义激活(自我):# 方法现在有副作用(向用户发送消息)self.status = '已激活'自我保存()send_mail('您的帐户已激活!', '...', [self.email])

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

  1. 我的数据库实体,持久性级别:我的应用程序保留哪些数据?
  2. 我的应用程序的实体,业务逻辑级别:我的应用程序有什么作用?

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

解决方案

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

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

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

关于领域模型

您需要认识到的第一件事是您的领域模型并不是真正关于数据;它与操作问题有关,例如激活此用户"、停用此用户"、当前激活了哪些用户?"和激活此用户".这个用户的名字是什么?".用经典术语来说:它是关于查询命令.

在命令中思考

让我们首先查看示例中的命令:激活此用户";和停用该用户".命令的好处在于它们可以很容易地通过小的给定何时然后场景来表达:

<块引用>

给定一个不活跃的用户
何时管理员激活此用户
然后用户变得活跃
向用户发送确认电子邮件
一个条目被添加到系统日志
(等等等等)

这种情况有助于了解您的基础架构的不同部分如何受到单个命令的影响 - 在这种情况下是您的数据库(某种活动"标志)、您的邮件服务器、您的系统日志等.

>

这样的场景也确实可以帮助您设置测试驱动开发环境.

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

表达命令

Django 提供了两种简单的命令表达方式;它们都是有效的选择,混合使用这两种方法并不罕见.

服务层

服务模块已经被@Hedde描述.这里定义了一个单独的模块,每个命令都表示为一个函数.

services.py

def activate_user(user_id):用户 = User.objects.get(pk=user_id)# 设置活动标志user.active = 真用户.save()# 邮件用户发邮件(...)#等等等等

使用表单

另一种方法是为每个命令使用一个 Django 表单.我更喜欢这种方法,因为它结合了多个密切相关的方面:

  • 执行命令(它做什么?)
  • 验证命令参数(它可以这样做吗?)
  • 命令演示(我该怎么做?)

forms.py

class ActivateUserForm(forms.Form):user_id = IntegerField(widget = UsernameSelectWidget,verbose_name=选择要激活的用户")# 用户名选择小部件不是标准的 Django 小部件,我只是编造的def clean_user_id(self):user_id = self.cleaned_data['user_id']如果 User.objects.get(pk=user_id).active:raise ValidationError(无法激活此用户")# 您还可以检查授权等.返回用户 IDdef执行(自我):"这不是表单 API 中的标准方法;它旨在取代'extract-data-from-form-in-view-and-do-stuff' 模式通过更可测试的模式."user_id = self.cleaned_data['user_id']用户 = User.objects.get(pk=user_id)# 设置活动标志user.active = 真用户.save()# 邮件用户发邮件(...)#等等等等

在查询中思考

你的例子没有包含任何查询,所以我冒昧地提出了一些有用的查询.我更喜欢使用术语问题",但查询是经典术语.有趣的查询是:这个用户的名字是什么?"、这个用户可以登录吗?"、显示已停用用户的列表"和已停用用户的地理分布是什么"?"

在开始回答这些问题之前,您应该始终问自己这个问题,是这样的:

  • 仅针对我的模板的演示查询,和/或
  • 与执行我的命令相关的业务逻辑查询,和/或
  • 一个报告查询.

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

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

进行查询

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

User.objects.filter(active=True)

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

自定义标签和过滤器

第一种选择对于仅用于展示的查询很有用:自定义标签和模板过滤器.

template.html

欢迎您,{{ user|friendly_name }}</h1>

template_tags.py

@register.filter定义友好名称(用户):返回 remote_api.get_cached_name(user.id)

查询方法

如果您的查询不仅仅是表示性的,您可以将查询添加到您的 services.py(如果您正在使用它),或者引入一个 queries.py 模块:

queries.py

def inactive_users():返回 User.objects.filter(active=False)def users_Called_publysher():对于 User.objects.all() 中的用户:如果 remote_api.get_cached_name(user.id) == publysher":产量用户

代理模型

代理模型在业务逻辑和报告的上下文中非常有用.您基本上定义了模型的增强子集.您可以通过覆盖 Manager.get_queryset() 方法.

models.py

class InactiveUserManager(models.Manager):def get_queryset(self):query_set = super(InactiveUserManager, self).get_queryset()返回 query_set.filter(active=False)类 InactiveUser(User):">>>对于 InactiveUser.objects.all() 中的用户:...断言 user.active 是 False"对象 = InactiveUserManager()元类:代理 = 真

查询模型

对于本质上复杂但经常执行的查询,存在查询模型的可能性.查询模型是一种非规范化形式,其中单个查询的相关数据存储在单独的模型中.诀窍当然是使非规范化模型与主要模型保持同步.仅当更改完全在您的控制之下时才能使用查询模型.

models.py

class InactiveUserDistribution(models.Model):country = CharField(max_length=200)inactive_user_count = IntegerField(默认值=0)

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

forms.py

class ActivateUserForm(forms.Form):# 看上面def执行(自我):# 看上面query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)query_model.inactive_user_count -= 1query_model.save()

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

signals.py

user_activated = Signal(providing_args = ['user'])user_deactivated = Signal(providing_args = ['user'])

forms.py

class ActivateUserForm(forms.Form):# 看上面def执行(自我):# 看上面user_activated.send_robust(发件人=自己,用户=用户)

models.py

class InactiveUserDistribution(models.Model):# 看上面@receiver(user_activated)def on_user_activated(sender, **kwargs):用户 = kwargs['用户']query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)query_model.inactive_user_count -= 1query_model.save()

保持清洁

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

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

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

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

一些参考资料

Django 文档:代理模型

Django 文档:信号

架构:领域驱动设计

I am writing a project in Django and I see that 80% of the code is in the file models.py. This code is confusing and, after a certain time, I cease to understand what is really happening.

Here is what bothers me:

  1. I find it ugly that my model level (which was supposed to be responsible only for the work with data from a database) is also sending email, walking on API to other services, etc.
  2. Also, I find it unacceptable to place business logic in the view, because this way it becomes difficult to control. For example, in my application there are at least three ways to create new instances of User, but technically it should create them uniformly.
  3. I do not always notice when the methods and properties of my models become non-deterministic and when they develop side effects.

Here is a simple example. At first, the User model was like this:

class User(db.Models):

    def get_present_name(self):
        return self.name or 'Anonymous'

    def activate(self):
        self.status = 'activated'
        self.save()

Over time, it turned into this:

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

What I want is to separate entities in my code:

  1. Entities of my database, persistence level: What data does my application keep?
  2. Entities of my application, business logic level: What does my application do?

What are the good practices to implement such an approach that can be applied in Django?

解决方案

It seems like you are asking about the difference between the data model and the domain model – the latter is where you can find the business logic and entities as perceived by your end user, the former is where you actually store your data.

Furthermore, I've interpreted the 3rd part of your question as: how to notice failure to keep these models separate.

These are two very different concepts and it's always hard to keep them separate. However, there are some common patterns and tools that can be used for this purpose.

About the Domain Model

The first thing you need to recognize is that your domain model is not really about data; it is about actions and questions such as "activate this user", "deactivate this user", "which users are currently activated?", and "what is this user's name?". In classical terms: it's about queries and commands.

Thinking in Commands

Let's start by looking at the commands in your example: "activate this user" and "deactivate this user". The nice thing about commands is that they can easily be expressed by small given-when-then scenario's:

given an inactive user
when the admin activates this user
then the user becomes active
and a confirmation e-mail is sent to the user
and an entry is added to the system log
(etc. etc.)

Such scenario's are useful to see how different parts of your infrastructure can be affected by a single command – in this case your database (some kind of 'active' flag), your mail server, your system log, etc.

Such scenario's also really help you in setting up a Test Driven Development environment.

And finally, thinking in commands really helps you create a task-oriented application. Your users will appreciate this :-)

Expressing Commands

Django provides two easy ways of expressing commands; they are both valid options and it is not unusual to mix the two approaches.

The service layer

The service module has already been described by @Hedde. Here you define a separate module and each command is represented as a function.

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

Using forms

The other way is to use a Django Form for each command. I prefer this approach, because it combines multiple closely related aspects:

  • execution of the command (what does it do?)
  • validation of the command parameters (can it do this?)
  • presentation of the command (how can I do this?)

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

Thinking in Queries

You example did not contain any queries, so I took the liberty of making up a few useful queries. I prefer to use the term "question", but queries is the classical terminology. Interesting queries are: "What is the name of this user?", "Can this user log in?", "Show me a list of deactivated users", and "What is the geographical distribution of deactivated users?"

Before embarking on answering these queries, you should always ask yourself this question, is this:

  • a presentational query just for my templates, and/or
  • a business logic query tied to executing my commands, and/or
  • a reporting query.

Presentational queries are merely made to improve the user interface. The answers to business logic queries directly affect the execution of your commands. Reporting queries are merely for analytical purposes and have looser time constraints. These categories are not mutually exclusive.

The other question is: "do I have complete control over the answers?" For example, when querying the user's name (in this context) we do not have any control over the outcome, because we rely on an external API.

Making Queries

The most basic query in Django is the use of the Manager object:

User.objects.filter(active=True)

Of course, this only works if the data is actually represented in your data model. This is not always the case. In those cases, you can consider the options below.

Custom tags and filters

The first alternative is useful for queries that are merely presentational: custom tags and template filters.

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)

Query methods

If your query is not merely presentational, you could add queries to your services.py (if you are using that), or introduce a queries.py module:

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 

Proxy models

Proxy models are very useful in the context of business logic and reporting. You basically define an enhanced subset of your model. You can override a Manager’s base QuerySet by overriding the Manager.get_queryset() method.

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

Query models

For queries that are inherently complex, but are executed quite often, there is the possibility of query models. A query model is a form of denormalization where relevant data for a single query is stored in a separate model. The trick of course is to keep the denormalized model in sync with the primary model. Query models can only be used if changes are entirely under your control.

models.py

class InactiveUserDistribution(models.Model):
    country = CharField(max_length=200)
    inactive_user_count = IntegerField(default=0)

The first option is to update these models in your commands. This is very useful if these models are only changed by one or two commands.

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

A better option would be to use custom signals. These signals are of course emitted by your commands. Signals have the advantage that you can keep multiple query models in sync with your original model. Furthermore, signal processing can be offloaded to background tasks, using Celery or similar frameworks.

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

Keeping it clean

When using this approach, it becomes ridiculously easy to determine if your code stays clean. Just follow these guidelines:

  • Does my model contain methods that do more than managing database state? You should extract a command.
  • Does my model contain properties that do not map to database fields? You should extract a query.
  • Does my model reference infrastructure that is not my database (such as mail)? You should extract a command.

The same goes for views (because views often suffer from the same problem).

  • Does my view actively manage database models? You should extract a command.

Some References

Django documentation: proxy models

Django documentation: signals

Architecture: Domain Driven Design

这篇关于django中业务逻辑和数据访问的分离的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆