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

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

我正在Django中编写一个项目,并且看到80%的代码在file中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中应用的方法?

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, database level: What contains my application?
  2. Entities of my application, business logic level: What can make my application?

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


回答 0

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

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

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

关于领域模型

您需要认识的第一件事是您的域模型并不是真正的数据。它涉及诸如“激活此用户”,“停用此用户”,“当前已激活哪些用户”和“该用户的名字是什么”之类的动作问题。用经典术语来说:它是关于查询命令的

指挥思维

让我们从示例中的命令开始:“激活此用户”和“停用此用户”。关于命令的好处是,它们可以很容易地用小给定的情况来表示:


管理员激活该用户时,将其指定为非活动用户
该用户将变为活动状态
并向该用户发送确认电子邮件,
并将条目添加到系统日志
(等)。

这种情况对于查看单个命令如何影响基础结构的不同部分很有用,在这种情况下,您的数据库(某种“活动”标志),邮件服务器,系统日志等会受到影响。

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

最后,思考命令确实可以帮助您创建面向任务的应用程序。您的用户将对此表示赞赏:-)

表达命令

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

  • 命令的执行(它做什么?)
  • 验证命令参数(可以执行此操作吗?)
  • 命令演示(如何执行此操作?)

表格

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(如果正在使用的话),或者引入querys.py模块:

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

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

表格

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

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

signal.py

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

表格

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

体系结构:域驱动设计

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 two questions: 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


回答 1

我通常在视图和模型之间实现服务层。这就像您项目的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 """
    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()

请注意,我通常将模型,视图和服务带到模块级别,并根据项目的规模进一步分开

I usually implement a service layer in between views and models. This acts like your project’s API and gives you a good helicopter view of what is going on. I inherited this practice from a colleague of mine that uses this layering technique a lot with Java projects (JSF), e.g:

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

Mind you, I usually take models, views and services to module level and separate even further depending on the project’s size


回答 2

首先,不要重复自己

然后,请注意不要过度设计,有时这只是浪费时间,并使某人失去对重要内容的关注。回顾python禅宗不时。

看一下活跃的项目

  • 更多的人=更多需要适当组织
  • 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 … etc
    • 也考虑分页过滤
  • 数据更新视图
    • 模板获取数据并相应地格式化
    • 甚至没有模板的API都是视图的一部分;例如tastypiepiston
    • 这也应该考虑中间件。

利用中间件 / 模板标签

  • 如果您需要为每个请求完成一些工作,那么中间件是一种解决方法。
    • 例如添加时间戳
    • 例如,更新有关网页点击量的指标
    • 例如,填充缓存
  • 如果您的代码片段总是在格式化对象时反复出现,那么模板标签就不错了。
    • 例如,活动选项卡/ 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

有关设计模式的更多信息

有关界面边界的更多信息

  • 您要使用的代码确实是模型的一部分吗?->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

或任何其他可以帮助您的东西;找到所需的接口边界将对您有所帮助。

First of all, Don’t repeat yourself.

Then, please be careful not to overengineer, sometimes it is just a waste of time, and makes someone lose focus on what is important. Review the zen of python from time to time.

Take a look at active projects

  • more people = more need to organize properly
  • the django repository they have a straightforward structure.
  • the pip repository they have a straigtforward directory structure.
  • the fabric repository is also a good one to look at.

    • you can place all your models under yourapp/models/logicalgroup.py
  • e.g User, Group and related models can go under yourapp/models/users.py
  • e.g Poll, Question, Answer … could go under yourapp/models/polls.py
  • load what you need in __all__ inside of yourapp/models/__init__.py

More about MVC

  • model is your data
    • this includes your actual data
    • this also includes your session / cookie / cache / fs / index data
  • user interacts with controller to manipulate the model
    • this could be an API, or a view that saves/updates your data
    • this can be tuned with request.GET / request.POST …etc
    • think paging or filtering too.
  • the data updates the view
    • the templates take the data and format it accordingly
    • APIs even w/o templates are part of the view; e.g. tastypie or piston
    • this should also account for the middleware.

Take advantage of middleware / templatetags

  • If you need some work to be done for each request, middleware is one way to go.
    • e.g. adding timestamps
    • e.g. updating metrics about page hits
    • e.g. populating a cache
  • If you have snippets of code that always reoccur for formatting objects, templatetags are good.
    • e.g. active tab / url breadcrumbs

Take advantage of model managers

  • creating User can go in a UserManager(models.Manager).
  • gory details for instances should go on the models.Model.
  • gory details for queryset could go in a models.Manager.
  • you might want to create a User one at a time, so you may think that it should live on the model itself, but when creating the object, you probably don’t have all the details:

Example:

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

Make use of forms where possible

A lot of boilerplate code can be eliminated if you have forms that map to a model. The ModelForm documentation is pretty good. Separating code for forms from model code can be good if you have a lot of customization (or sometimes avoid cyclic import errors for more advanced uses).

Use management commands when possible

  • e.g. yourapp/management/commands/createsuperuser.py
  • e.g. yourapp/management/commands/activateinbulk.py

if you have business logic, you can separate it out

  • django.contrib.auth uses backends, just like db has a backend…etc.
  • add a setting for your business logic (e.g. AUTHENTICATION_BACKENDS)
  • you could use django.contrib.auth.backends.RemoteUserBackend
  • you could use yourapp.backends.remote_api.RemoteUserBackend
  • you could use yourapp.backends.memcached.RemoteUserBackend
  • delegate the difficult business logic to the backend
  • make sure to set the expectation right on the input/output.
  • changing business logic is as simple as changing a setting :)

backend example:

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' 

could become:

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

more about design patterns

more about interface boundaries

  • Is the code you want to use really part of the models? -> yourapp.models
  • Is the code part of business logic? -> yourapp.vendor
  • Is the code part of generic tools / libs? -> yourapp.libs
  • Is the code part of business logic libs? -> yourapp.libs.vendor or yourapp.vendor.libs
  • Here is a good one: can you test your code independently?
    • yes, good :)
    • no, you may have an interface problem
    • when there is clear separation, unittest should be a breeze with the use of mocking
  • Is the separation logical?
    • yes, good :)
    • no, you may have trouble testing those logical concepts separately.
  • Do you think you will need to refactor when you get 10x more code?
    • yes, no good, no bueno, refactor could be a lot of work
    • no, that’s just awesome!

In short, you could have

  • 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

or anything else that helps you; finding the interfaces you need and the boundaries will help you.


回答 3

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

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

Django employs a slightly modified kind of MVC. There’s no concept of a “controller” in Django. The closest proxy is a “view”, which tends to cause confusion with MVC converts because in MVC a view is more like Django’s “template”.

In Django, a “model” is not merely a database abstraction. In some respects, it shares duty with the Django’s “view” as the controller of MVC. It holds the entirety of behavior associated with an instance. If that instance needs to interact with an external API as part of it’s behavior, then that’s still model code. In fact, models aren’t required to interact with the database at all, so you could conceivable have models that entirely exist as an interactive layer to an external API. It’s a much more free concept of a “model”.


回答 4

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

在Django中,MVC是通过以下方式实现的:

视图层分为两部分。该视图仅应用于管理HTTP请求,它们将被调用并对其进行响应。视图与应用程序的其余部分(表单,模型表单,自定义类,在简单情况下直接与模型)进行通信。要创建界面,我们使用模板。模板就像Django的字符串一样,它将一个上下文映射到其中,并且该上下文由应用程序传达给视图(当视图询问时)。

模型层提供封装,抽象,验证,智能,并使您的数据面向对象(他们说有朝一日DBMS也将面向对象)。这并不意味着您应该制作巨大的models.py文件(实际上,一个很好的建议是将模型分成不同的文件,将它们放入名为“ models”的文件夹中,在其中创建一个“ __init__.py”文件导入所有模型并最终使用models.Model类的属性“ app_label”的文件夹)。模型应该使您摆脱对数据的操作,这将使您的应用程序更简单。如果需要,您还应该为模型创建外部类,例如“工具”。您还可以在模型中使用继承,将模型的Meta类的“抽象”属性设置为“真”。

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

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

我的建议是:尽可能多地阅读代码,周围有很多django应用程序,但是不要那么认真地对待它们。每种情况都是不同的,模式和理论会有所帮助,但并非总是如此,这不是很精确,django只是为您提供了一些有用的工具,您可以使用这些工具来减轻一些麻烦(例如管理界面,Web表单验证,i18n,观察者模式实施,所有以及之前提到的内容和其他内容),但是好的设计来自经验丰富的设计师。

PS .:使用auth应用程序中的“ User”类(来自标准django),您可以创建用户个人资料,或者至少读取其代码,这对您的情况很有用。

In Django, MVC structure is as Chris Pratt said, different from classical MVC model used in other frameworks, I think the main reason for doing this is avoiding a too strict application structure, like happens in others MVC frameworks like CakePHP.

In Django, MVC was implemented in the following way:

View layer is splitted in two. The views should be used only to manage HTTP requests, they are called and respond to them. Views communicate with the rest of your application (forms, modelforms, custom classes, of in simple cases directly with models). To create the interface we use Templates. Templates are string-like to Django, it maps a context into them, and this context was communicated to the view by the application (when view asks).

Model layer gives encapsulation, abstraction, validation, intelligence and makes your data object-oriented (they say someday DBMS will also). This doesn’t means that you should make huge models.py files (in fact a very good advice is to split your models in different files, put them into a folder called ‘models’, make an ‘__init__.py’ file into this folder where you import all your models and finally use the attribute ‘app_label’ of models.Model class). Model should abstract you from operating with data, it will make your application simpler. You should also, if required, create external classes, like “tools” for your models.You can also use heritage in models, setting the ‘abstract’ attribute of your model’s Meta class to ‘True’.

Where is the rest? Well, small web applications generally are a sort of an interface to data, in some small program cases using views to query or insert data would be enough. More common cases will use Forms or ModelForms, which are actually “controllers”. This is not other than a practical solution to a common problem, and a very fast one. It’s what a website use to do.

If Forms are not enogh for you, then you should create your own classes to do the magic, a very good example of this is admin application: you can read ModelAmin code, this actually works as a controller. There is not a standard structure, I suggest you to examine existing Django apps, it depends on each case. This is what Django developers intended, you can add xml parser class, an API connector class, add Celery for performing tasks, twisted for a reactor-based application, use only the ORM, make a web service, modify the admin application and more… It’s your responsability to make good quality code, respect MVC philosophy or not, make it module based and creating your own abstraction layers. It’s very flexible.

My advice: read as much code as you can, there are lots of django applications around, but don’t take them so seriously. Each case is different, patterns and theory helps, but not always, this is an imprecise cience, django just provide you good tools that you can use to aliviate some pains (like admin interface, web form validation, i18n, observer pattern implementation, all the previously mentioned and others), but good designs come from experienced designers.

PS.: use ‘User’ class from auth application (from standard django), you can make for example user profiles, or at least read its code, it will be useful for your case.


回答 5

一个古老的问题,但是我还是想提供我的解决方案。基于接受,模型对象也需要一些其他功能,而将它们放置在models.py中很尴尬。可以根据个人喜好单独编写繁琐的业务逻辑,但是我至少喜欢该模型来完成与自身相关的所有事情。该解决方案还为那些喜欢将所有逻辑放在模型中的人提供支持。

因此,我设计了一种hack,使我可以将逻辑与模型定义分开,并且仍然可以从IDE中获得所有提示。

优点应该很明显,但这列出了我观察到的一些优点:

  • 数据库定义仅保留了这一点-没有附加逻辑“垃圾”
  • 与模型相关的逻辑都整齐地放在一个地方
  • 所有服务(表单,REST,视图)都具有单个逻辑访问点
  • 最棒的是:一旦意识到我的models.py变得过于混乱并且不必将逻辑分开,就不必重写任何代码。分离是平滑且迭代的:我可以一次执行一个函数,也可以一次执行整个类或整个model.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 ...

app / logic / 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参数指定类型的小小的麻烦。

An old question, but I’d like to offer my solution anyway. It’s based on acceptance that model objects too require some additional functionality while it’s awkward to place it within the models.py. Heavy business logic may be written separately depending on personal taste, but I at least like the model to do everything related to itself. This solution also supports those who like to have all the logic placed within models themselves.

As such, I devised a hack that allows me to separate logic from model definitions and still get all the hinting from my IDE.

The advantages should be obvious, but this lists a few that I have observed:

  • DB definitions remain just that – no logic “garbage” attached
  • Model-related logic is all placed neatly in one place
  • All the services (forms, REST, views) have a single access point to logic
  • Best of all: I did not have to rewrite any code once I realised that my models.py became too cluttered and had to separate the logic away. The separation is smooth and iterative: I could do a function at a time or entire class or the entire models.py.

I have been using this with Python 3.4 and greater and Django 1.8 and greater.

app/models.py

....
from app.logic.user import UserLogic

class User(models.Model, UserLogic):
    field1 = models.AnyField(....)
    ... field definitions ...

app/logic/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 ...

The only thing I can’t figure out is how to make my IDE (PyCharm in this case) recognise that UserLogic is actually User model. But since this is obviously a hack, I’m quite happy to accept the little nuisance of always specifying type for self parameter.


回答 6

我必须同意你的看法。Django有很多可能性,但是最好的起点是回顾Django的设计理念

  1. 从模型属性调用API是不理想的,似乎在视图中执行这样的事情并可能创建一个服务层来保持干燥是更有意义的。如果对API的调用是非阻塞的,并且调用成本很高,则可以将请求发送给服务工作者(从队列中消耗资源的工作者)。

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

包括所有相关领域逻辑

模型应遵循Martin Fowler的Active Record设计模式来封装“对象”的各个方面。

  1. 您描述的副作用是显而易见的,这里的逻辑可以更好地分解为Querysets和manager。这是一个例子:

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

    管理员

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

I would have to agree with you. There are a lot of possibilities in django but best place to start is reviewing Django’s design philosophy.

  1. Calling an API from a model property would not be ideal, it seems like it would make more sense to do something like this in the view and possibly create a service layer to keep things dry. If the call to the API is non-blocking and the call is an expensive one, sending the request to a service worker (a worker that consumes from a queue) might make sense.

  2. As per Django’s design philosophy models encapsulate every aspect of an “object”. So all business logic related to that object should live there:

Include all relevant domain logic

Models should encapsulate every aspect of an “object,” following Martin Fowler’s Active Record design pattern.

  1. The side effects you describe are apparent, the logic here could be better broken down into Querysets and managers. Here is an example:

    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)
    

回答 7

我大多同意选择的答案(https://stackoverflow.com/a/12857584/871392),但想在“进行查询”部分中添加选项。

可以为模型定义QuerySet类,以进行过滤器查询等。之后,您可以将此查询集类代理给模型的管理器,就像内置管理器和QuerySet类一样。

虽然,如果必须查询多个数据模型以获得一个域模型,对我来说,将其放在像以前建议的那样的单独模块中似乎更合理。

I’m mostly agree with chosen answer (https://stackoverflow.com/a/12857584/871392), but want to add option in Making Queries section.

One can define QuerySet classes for models for make filter queries and so on. After that you can proxy this queryset class for model’s manager, like build-in Manager and QuerySet classes do.

Although, if you had to query several data models to get one domain model, it seems more reasonable to me to put this in separate module like suggested before.


回答 8

关于优缺点的不同选择的最全面的文章:

  1. 想法1:胖模型
  2. 想法2:将业务逻辑放入视图/表单
  3. 理念3:服务
  4. 理念4:QuerySet / Manager
  5. 结论

资料来源:https : //sunscrapers.com/blog/where-to-put-business-logic-django/

Most comprehensive article on the different options with pros and cons:

  1. Idea #1: Fat Models
  2. Idea #2: Putting Business Logic in Views/Forms
  3. Idea #3: Services
  4. Idea #4: QuerySets/Managers
  5. Conclusion

Source: https://sunscrapers.com/blog/where-to-put-business-logic-django/


回答 9

Django设计用于轻松交付网页。如果您对此不满意,则应该使用其他解决方案。

我在模型上写根或通用操作(具有相同的接口),在模型控制器上写其他操作。如果需要其他模型的操作,请导入其控制器。

这种方法对我和应用程序的复杂性已经足够。

Hedde的回应是一个示例,展示了django和python本身的灵活性。

无论如何,这是一个非常有趣的问题!

Django is designed to be easely used to deliver web pages. If you are not confortable with this perhaps you should use another solution.

I’m writting the root or common operations on the model (to have the same interface) and the others on the controller of the model. If I need an operation from other model I import its controller.

This approach it’s enough for me and the complexity of my applications.

Hedde’s response is an example that shows the flexibility of django and python itself.

Very interesting question anyway!