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

以下是让我感到困扰的是:

  1. 我觉得我的模特水平(本该是 仅负责处理来自数据库的数据)还 发送邮箱、使用API访问其他服务等.
  2. 此外,我发现在视图中放置业务逻辑是不可接受的,因为
  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. 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?

在Django实施这种方法的良好实践是什么?

推荐答案

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

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

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

关于域模型

首先你需要认识到你的领域模型并不是关于数据的;大约是actionsquestions,例如"激活此用户"、"停用此用户"、"当前激活了哪些用户?",还有"这个用户叫什么名字?".用classic 术语来说:大约是queriescommands.

用命令思考

让我们先看看示例中的命令:"激活此用户"和"停用此用户".命令的好处在于,当场景为:

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

这样的场景有助于了解单个命令如何影响基础设施的不同部分——在本例中是数据库(某种"活动"标志)、邮件服务器、系统日志(log)等.

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

最后,在命令中思考确实可以帮助您创建面向任务的应用程序.您的用户将会喜欢这一点:-)

表达命令

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

服务层

service module已经是described by @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

在质疑中思考

您的示例没有包含任何查询,因此我冒昧地编了几个有用的查询.我更喜欢用"问题"这个词,但查询是classic 的术语.有趣的问题是:"这个用户叫什么名字?","此用户可以登录吗?","显示停用用户列表"和"停用用户的地理分布如何?"

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

  • 仅针对我的模板的presentational查询,和/或
  • 与执行我的命令相关的business logic个查询,和/或
  • reporting个问题.

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

另一个问题是:"我能完全控制答案吗?"例如,在查询用户名时(在此上下文中),我们无法控制结果,因为我们依赖于外部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()方法来重写管理器的基本查询集.

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

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

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

保持清洁

使用此方法时,确定代码是否保持干净变得非常容易.只需遵循以下指导原则:

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

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

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

一些参考文献

Django documentation: proxy models

Django documentation: signals

Architecture: Domain Driven Design

Python相关问答推荐

将C struct 的指针传递给Python中的ioctel

从 struct 类型创建MultiPolygon对象,并使用Polars列出[list[f64]列

这家einsum运营在做什么?E = NP.einsum(aj,kl-il,A,B)

在上下文管理器中更改异常类型

Python -Polars库中的滚动索引?

如何才能知道Python中2列表中的巧合.顺序很重要,但当1个失败时,其余的不应该失败或是0巧合

Odoo 14 hr. emergency.public内的二进制字段

如何比较numPy数组中的两个图像以获取它们不同的像素

处理(潜在)不断增长的任务队列的并行/并行方法

为什么sys.exit()不能与subproccess.run()或subprocess.call()一起使用

Python虚拟环境的轻量级使用

Godot:需要碰撞的对象的AdditionerBody2D或Area2D以及queue_free?

海上重叠直方图

python中字符串的条件替换

无法连接到Keycloat服务器

在不同的帧B中判断帧A中的子字符串,每个帧的大小不同

pysnmp—lextudio使用next()和getCmd()生成器导致TypeError:tuple对象不是迭代器''

Polars map_使用多处理对UDF进行批处理

使用__json__的 pyramid 在客户端返回意外格式

Beautifulsoup:遍历一个列表,从a到z,并解析数据,以便将其存储在pdf中.