本文翻译自:Separation of business logic and data access in django
I am writing a project in Django and I see that 80% of the code is in the file models.py
. 我正在Django编写一个项目,我发现80%的代码都在models.py
文件models.py
。 This code is confusing and, after a certain time, I cease to understand what is really happening. 这段代码令人困惑,经过一段时间后,我不再明白究竟发生了什么。
Here is what bothers me: 这是困扰我的:
User
, but technically it should create them uniformly. 例如,在我的应用程序中,至少有三种方法可以创建User
新实例,但从技术上讲,它应该统一创建它们。 Here is a simple example. 这是一个简单的例子。 At first, the User
model was like this: 起初, User
模型是这样的:
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: 我想要的是在我的代码中分离实体:
What are the good practices to implement such an approach that can be applied in Django? 实现可以在Django中应用的这种方法有哪些好的做法?
参考:https://stackoom.com/question/qmLc/在django中分离业务逻辑和数据访问
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. 这就像您的项目的API一样,可以让您直观地了解正在发生的事情。 I inherited this practice from a colleague of mine that uses this layering technique a lot with Java projects (JSF), eg: 我从我的同事那里继承了这种做法,这种做法在Java项目(JSF)中使用了这种分层技术,例如:
models.py models.py
class Book:
author = models.ForeignKey(User)
title = models.CharField(max_length=125)
class Meta:
app_label = "library"
services.py 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 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 请注意,我通常会将模型,视图和服务提供给模块级别,并根据项目的大小进一步分离
Django is designed to be easely used to deliver web pages. Django旨在轻松地用于提供网页。 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. Hedde的回答是一个例子,展示了django和python本身的灵活性。
Very interesting question anyway! 反正非常有趣的问题!
Django employs a slightly modified kind of MVC. Django使用了一种稍微修改过的MVC。 There's no concept of a "controller" in Django. 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". 最接近的代理是“视图”,这往往会导致与MVC转换混淆,因为在MVC中,视图更像是Django的“模板”。
In Django, a "model" is not merely a database abstraction. 在Django中,“模型”不仅仅是数据库抽象。 In some respects, it shares duty with the Django's "view" as the controller of MVC. 在某些方面,它与Django作为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. 如果该实例需要与外部API交互作为其行为的一部分,那么这仍然是模型代码。 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. 事实上,模型根本不需要与数据库交互,因此您可以想象将模型完全作为外部API的交互层存在。 It's a much more free concept of a "model". 这是一个更“自由”的“模型”概念。
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 看看活跃的项目
the fabric repository is also a good one to look at. 结构存储库也是一个很好看的。
yourapp/models/logicalgroup.py
您可以将所有模型放在yourapp/models/logicalgroup.py
User
, Group
and related models can go under yourapp/models/users.py
例如, User
, Group
和相关模型可以在yourapp/models/users.py
Poll
, Question
, Answer
... could go under yourapp/models/polls.py
例如Poll
, Question
, Answer
......可以在yourapp/models/polls.py
__all__
inside of yourapp/models/__init__.py
在yourapp/models/__init__.py
中的__all__
加载你需要的yourapp/models/__init__.py
More about MVC 更多关于MVC的信息
request.GET
/ request.POST
...etc 这可以通过request.GET
/ request.POST
...等来调整 tastypie
or piston
例如tastypie
或piston
Take advantage of middleware / templatetags 利用中间件 / 模板标签
Take advantage of model managers 利用模型经理
User
can go in a UserManager(models.Manager)
. 创建User
可以进入UserManager(models.Manager)
。 models.Model
. 实例的血腥细节应该放在models.Model
。 queryset
could go in a models.Manager
. 对于血淋淋的细节queryset
可以去在models.Manager
。 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: 您可能希望一次创建一个User
,因此您可能认为它应该存在于模型本身上,但是在创建对象时,您可能没有所有细节: 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. ModelForm documentation
非常好。 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 尽可能使用管理命令
yourapp/management/commands/createsuperuser.py
例如yourapp/management/commands/createsuperuser.py
yourapp/management/commands/activateinbulk.py
例如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. django.contrib.auth
使用后端 ,就像db有后端......等。 setting
for your business logic (eg AUTHENTICATION_BACKENDS
) 为您的业务逻辑添加setting
(例如AUTHENTICATION_BACKENDS
) django.contrib.auth.backends.RemoteUserBackend
你可以使用django.contrib.auth.backends.RemoteUserBackend
yourapp.backends.remote_api.RemoteUserBackend
你可以使用yourapp.backends.remote_api.RemoteUserBackend
yourapp.backends.memcached.RemoteUserBackend
你可以使用yourapp.backends.memcached.RemoteUserBackend
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 更多关于界面边界
yourapp.models
- > yourapp.models
yourapp.vendor
- > yourapp.vendor
yourapp.libs
- > yourapp.libs
yourapp.libs.vendor
or yourapp.vendor.libs
- > yourapp.libs.vendor
或yourapp.vendor.libs
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. 找到您需要的接口和边界将有助于您。
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. 此外,我已将您问题的第3部分解释为:如何注意未能将这些模型分开。
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. 但是,有一些常用的模式和工具可用于此目的。
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 . 用经典术语来说:它是关于查询和命令的 。
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 :-) 您的用户会很感激:-)
Django provides two easy ways of expressing commands; Django提供了两种表达命令的简单方法; they are both valid options and it is not unusual to mix the two approaches. 它们都是有效的选择,并且混合这两种方法并不罕见。
The service module has already been described by @Hedde . @Hedde已经描述了 服务模块 。 Here you define a separate module and each command is represented as a function. 在这里,您可以定义一个单独的模块,每个命令都表示为一个函数。
services.py 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
The other way is to use a Django Form for each command. 另一种方法是为每个命令使用Django表单。 I prefer this approach, because it combines multiple closely related aspects: 我更喜欢这种方法,因为它结合了多个密切相关的方面:
forms.py 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
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. 例如,在查询用户名时(在此上下文中),我们无法控制结果,因为我们依赖于外部API。
The most basic query in Django is the use of the Manager object: Django中最基本的查询是使用Manager对象:
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. 在这些情况下,您可以考虑以下选项。
The first alternative is useful for queries that are merely presentational: custom tags and template filters. 第一种替代方法对于仅仅是表示的查询非常有用:自定义标记和模板过滤器。
template.html template.html
Welcome, {{ user|friendly_name }}
template_tags.py template_tags.py
@register.filter
def friendly_name(user):
return remote_api.get_cached_name(user.id)
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: 如果您的查询不仅仅是表示性的,您可以向services.py添加查询(如果您正在使用它),或者引入queries.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
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. 您可以通过覆盖Manager.get_queryset()
方法来覆盖Manager的基本QuerySet。
models.py 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
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 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 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. 此外,可以使用Celery或类似的框架将信号处理卸载到后台任务。
signals.py signals.py
user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])
forms.py forms.py
class ActivateUserForm(forms.Form):
# see above
def execute(self):
# see above
user_activated.send_robust(sender=self, user=user)
models.py 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()
When using this approach, it becomes ridiculously easy to determine if your code stays clean. 使用这种方法时,确定代码是否保持干净变得非常容易。 Just follow these guidelines: 请遵循以下准则:
The same goes for views (because views often suffer from the same problem). 视图也是如此(因为视图经常遇到同样的问题)。
Django documentation: proxy models Django文档:代理模型
Django documentation: signals Django文档:信号
Architecture: Domain Driven Design 架构:领域驱动设计