Separation of business logic and data access in django
我正在Django编写一个项目,我看到80%的代码都在
以下是困扰我的问题:
下面是一个简单的例子。起初,
1 2 3 4 5 6 7 8 | class User(db.Models): def get_present_name(self): return self.name or 'Anonymous' def activate(self): self.status = 'activated' self.save() |
随着时间的推移,它变成了:
1 2 3 4 5 6 7 8 9 10 11 12 | 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]) |
我想要的是在代码中分离实体:
在Django实施这种方法的良好实践是什么?
似乎您在询问数据模型和域模型之间的区别——后者是您可以找到最终用户所感知的业务逻辑和实体的地方,前者是您实际存储数据的地方。好的。
此外,我将您问题的第三部分解释为:如何注意到未能将这些模型分开。好的。
这是两个非常不同的概念,总是很难将它们分开。但是,有一些通用的模式和工具可以用于此目的。好的。关于域模型
您需要认识到的第一件事是,您的域模型实际上并不涉及数据;它涉及诸如"激活此用户"、"停用此用户"、"当前激活了哪些用户"等操作和问题。,以及"此用户的名称是什么?".在经典术语中:它是关于查询和命令的。好的。在命令中思考
让我们从示例中的命令开始:"激活此用户"和"停用此用户"。关于命令的好处在于,它们可以很容易地用小的给定值来表示,而在这种情况下:好的。
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.)Ok.
这种场景对于了解单个命令如何影响基础结构的不同部分非常有用——在这种情况下,您的数据库(某种"活动"标志)、邮件服务器、系统日志等。好的。
这样的场景还可以帮助您建立一个测试驱动的开发环境。好的。
最后,使用命令进行思考确实有助于创建面向任务的应用程序。您的用户将对此表示感谢:—)好的。表达命令
Django提供了两种简单的命令表达方式;它们都是有效的选项,混合使用这两种方法并不少见。好的。服务层
@hedde已经描述了服务模块。在这里,您定义一个单独的模块,每个命令都表示为一个函数。好的。
服务Py好的。
1 2 3 4 5 6 7 8 9 10 11 | 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表单。我更喜欢这种方法,因为它结合了多个密切相关的方面:好的。
- 执行命令(它做什么?)
- 命令参数的验证(它能做到吗?)
- 命令的表示(我如何才能这样做?)
表单好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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对象:好的。
1 | User.objects.filter(active=True) |
当然,只有当数据在数据模型中实际表示时,这才有效。情况并非总是如此。在这些情况下,您可以考虑下面的选项。好的。自定义标记和筛选器
第一种选择对于仅具有表示性的查询很有用:自定义标记和模板过滤器。好的。
模板语言好的。
1 | Welcome, {{ user|friendly_name }} |
模板_tags.py好的。
1 2 3 | @register.filter def friendly_name(user): return remote_api.get_cached_name(user.id) |
查询方法
如果您的查询不仅仅是表示性的,您可以将查询添加到services.py(如果您正在使用它),或者引入querys.py模块:好的。
质询好的。
1 2 3 4 5 6 7 8 | 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 |
代理模型
代理模型在业务逻辑和报告环境中非常有用。您基本上定义了模型的增强子集。您可以通过重写
模特儿好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 |
查询模型
对于本质上很复杂但经常执行的查询,存在查询模型的可能性。查询模型是一种非规范化形式,其中单个查询的相关数据存储在单独的模型中。当然,技巧是使非规范化模型与主模型保持同步。只有当更改完全在您的控制之下时,才能使用查询模型。好的。
模特儿好的。
1 2 3 | class InactiveUserDistribution(models.Model): country = CharField(max_length=200) inactive_user_count = IntegerField(default=0) |
第一个选项是在命令中更新这些模型。如果这些模型仅由一个或两个命令更改,那么这非常有用。好的。
表单好的。
1 2 3 4 5 6 7 8 | 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() |
更好的选择是使用自定义信号。这些信号当然是由你的命令发出的。信号的优点是可以使多个查询模型与原始模型保持同步。此外,可以使用芹菜或类似的框架将信号处理卸载到后台任务。好的。
信号:Py好的。
1 2 | user_activated = Signal(providing_args = ['user']) user_deactivated = Signal(providing_args = ['user']) |
表单好的。
1 2 3 4 5 6 | class ActivateUserForm(forms.Form): # see above def execute(self): # see above user_activated.send_robust(sender=self, user=user) |
模特儿好的。
1 2 3 4 5 6 7 8 9 | 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文件:信号好的。
体系结构:领域驱动设计好的。好啊。
我通常在视图和模型之间实现一个服务层。这就像你的项目的API,给你一个很好的直升机视角,了解正在发生的事情。我从我的一个同事那里继承了这个实践,他使用这个分层技术大量使用Java项目(JSF),例如:
模特儿
1 2 3 4 5 6 | class Book: author = models.ForeignKey(User) title = models.CharField(max_length=125) class Meta: app_label ="library" |
服务Py
1 2 3 4 5 6 7 | 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) |
VIEW
1 2 3 4 5 | 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
首先,不要重复你自己。好的。
那么,请注意不要过度设计,有时这只是浪费时间,并使某人失去对重要的东西的关注。不时地回顾一下python的禅。好的。
查看活动项目好的。
- 更多人=更多人需要正确组织
- Django存储库有一个简单的结构。
- PIP存储库有一个strightforward目录结构。
结构存储库也是一个很好的库。好的。
- 您可以将您的所有模型放在
yourapp/models/logicalgroup.py 下。
- 您可以将您的所有模型放在
- 如
User 、Group 及相关型号可归入yourapp/models/users.py 项下。 - 如
Poll 、Question 、Answer …可能在yourapp/models/polls.py 之下 - 在
yourapp/models/__init__.py 的内部加载您在__all__ 中需要的内容。
更多关于MVC好的。
- 模型是你的数据
- 这包括您的实际数据
- 这还包括会话/cookie/cache/fs/index数据
- 用户与控制器交互操作模型
- 这可以是一个API,也可以是一个保存/更新数据的视图。
- 这可以通过
request.GET 或request.POST 等进行调整。 - 也可以考虑分页或过滤。
- 数据更新视图
- 模板获取数据并相应地格式化它。
- API,即使是w/o模板也是视图的一部分;例如,
tastypie 或piston 。 - 这也应该解释中间件。
利用中间件/模板标记好的。
- 如果您需要为每个请求做一些工作,中间件是一种方法。
- 例如,添加时间戳
- 例如,更新有关页面点击率的指标
- 例如,填充缓存
- 如果您有格式化对象时总是重复出现的代码片段,则templateTags很好。
- 例如,活动选项卡/URL面包屑
利用模型管理器好的。
- 创建
User 可以在UserManager(models.Manager) 中进行。 - 实例的详细信息应该放在
models.Model 上。 - 有关
queryset 的详细信息可以放在models.Manager 中。 - 您可能希望一次创建一个
User ,因此您可能认为它应该活在模型本身上,但在创建对象时,您可能不知道所有细节:
例子:好的。
1 2 3 4 5 6 7 8 9 10 | 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() |
尽可能使用表格好的。
如果您有映射到模型的表单,那么可以消除许多样板代码。
尽可能使用管理命令好的。
- 如
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 。 - 将困难的业务逻辑委托给后端
- 确保在输入/输出上正确设置期望值。
- 更改业务逻辑和更改设置一样简单:)
后端示例:好的。
1 2 3 4 5 | 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' |
可能成为:好的。
1 2 3 4 5 6 7 8 | 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 |
有关设计模式的详细信息好的。
- 关于设计模式已经有一个很好的问题了
- 关于实用设计模式的非常好的视频
- Django的后端明显使用了委托设计模式。
有关接口边界的详细信息好的。
- 您想要使用的代码真的是模型的一部分吗?->
yourapp.models 。 - 代码是业务逻辑的一部分吗?->江户十一〔一〕号
- 代码是通用工具/libs的一部分吗?->埃多克斯1〔2〕
- 代码是业务逻辑库的一部分吗?->
yourapp.libs.vendor 或yourapp.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
或者其他任何有助于您的东西;找到您需要的接口和边界将帮助您。好的。好啊。
Django采用了一种稍加修改的MVC。Django没有"控制器"的概念。最近的代理是一个"视图",这往往会导致与MVC转换混淆,因为在MVC中,视图更像Django的"模板"。
在Django中,"模型"不仅仅是数据库抽象。在某些方面,它与Django作为MVC控制者的"观点"共享职责。它保存与实例关联的全部行为。如果该实例需要与外部API交互作为其行为的一部分,那么这仍然是模型代码。事实上,模型根本不需要与数据库交互,因此您可以想象,拥有完全作为外部API交互层存在的模型。这是一个更自由的概念"模型"。
在Django中,MVC结构正如Chris Pratt所说,不同于其他框架中使用的经典MVC模型,我认为这样做的主要原因是避免了过于严格的应用程序结构,就像在Cakephp等其他MVC框架中一样。
在Django,MVC的实施方式如下:
视图层分为两部分。视图应该只用于管理HTTP请求,它们被调用并响应。视图与应用程序的其余部分通信(窗体、模型窗体、自定义类,在简单情况下直接与模型通信)。为了创建接口,我们使用模板。模板是类似于django的字符串,它将上下文映射到模板中,并且该上下文由应用程序(当视图请求时)与视图通信。
模型层提供了封装、抽象、验证、智能,并使您的数据面向对象(他们说有一天DBMS也会这样做)。这并不意味着你应该制作巨大的models.py文件(事实上,一个非常好的建议是将你的模型分割成不同的文件,将它们放在一个名为"models"的文件夹中,将一个"uuu init_uuuu.py"文件放在这个文件夹中,在这个文件夹中你导入所有模型,最后使用models.model类的属性"app_label"。模型应该从使用数据的操作中抽象出您,它将使您的应用程序更简单。如果需要,还应该为模型创建外部类,如"工具"。还可以在模型中使用Heritage,将模型的元类的"abstract"属性设置为"true"。
其余的在哪里?嗯,小型Web应用程序通常是一种数据接口,在某些小型程序中,使用视图查询或插入数据就足够了。更常见的情况是使用窗体或模型窗体,它们实际上是"控制器"。这不是一个解决常见问题的实用方法,也是一个非常快速的方法。这是网站用来做的。
如果表单不适合您,那么您应该创建自己的类来实现这一点,管理应用程序就是一个很好的例子:您可以读取modelamin代码,这实际上是一个控制器。没有一个标准的结构,我建议您检查现有的django应用程序,这取决于每种情况。这就是Django开发人员的意图,您可以添加XML解析器类、API连接器类、为执行任务添加芹菜、为基于reactor的应用程序扭曲、仅使用ORM、生成Web服务、修改管理应用程序等等…你的责任是编写好质量的代码,是否尊重MVC的理念,使它基于模块,并创建你自己的抽象层。它非常灵活。
我的建议:尽可能多地阅读代码,周围有很多django应用程序,但不要太认真地对待它们。每种情况都是不同的,模式和理论有助于,但并非总是如此,这是一个不精确的科学,Django只是为您提供了好的工具,您可以使用这些工具来减轻一些痛苦(如管理界面、Web表单验证、i18n、观察者模式实现、前面提到的所有以及其他),但好的设计来自经验丰富的设计师。
ps.:使用auth应用程序中的'user'类(来自标准django),您可以创建例如用户配置文件,或者至少读取其代码,它将对您的案例有用。
我必须同意你的意见。Django有很多可能性,但最好的开始是回顾Django的设计理念。
从模型属性调用API并不理想,在视图中这样做似乎更有意义,并且可能创建一个服务层来保持事物的干燥。如果对API的调用是非阻塞的,并且调用很昂贵,那么将请求发送给服务工作者(从队列中消费的工作者)可能是有意义的。
根据Django的设计哲学模型,它封装了"对象"的各个方面。因此,与该对象相关的所有业务逻辑都应该存在于该对象中:
Include all relevant domain logic
Models should encapsulate every aspect of an"object," following Martin Fowler’s Active Record design pattern.
您描述的副作用很明显,这里的逻辑可以更好地分解为查询集和管理器。下面是一个例子:
模特儿
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | 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() |
行政管理部门
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # 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) |
我基本上同意所选的答案(https://stackoverflow.com/a/12857584/871392),但希望在"生成查询"部分添加选项。
可以为模型定义queryset类,以便进行筛选查询和打开子项。之后,您可以为模型的管理器代理这个查询集类,就像内置管理器和查询集类一样。
尽管如此,如果您必须查询多个数据模型才能获得一个域模型,那么将其放入前面建议的单独模块中似乎更为合理。
一个老问题,但我还是想提出我的解决方案。这是基于接受模型对象也需要一些额外的功能,而将其放在models.py中则很难。繁重的业务逻辑可以根据个人喜好单独编写,但我至少喜欢这个模型做与自身相关的所有事情。这个解决方案还支持那些喜欢将所有逻辑放在模型中的人。
因此,我设计了一个黑客程序,它允许我将逻辑与模型定义分开,并且仍然从我的IDE中得到所有提示。
优势应该是显而易见的,但下面列出了一些我观察到的:
- DB定义保持不变-没有附加逻辑"垃圾"。
- 模型相关逻辑都整齐地放在一个地方
- 所有服务(窗体、REST、视图)都有一个逻辑访问点。
- 最重要的是:当我意识到models.py变得过于混乱,不得不将逻辑分离开来时,我不必重写任何代码。分离是平滑和迭代的:我可以在一个时间或者整个类或者整个模型上做一个函数。
我在Python3.4及更高版本和django1.8及更高版本中使用过这个。
App/MealthsPy
1 2 3 4 5 6 | .... from app.logic.user import UserLogic class User(models.Model, UserLogic): field1 = models.AnyField(....) ... field definitions ... |
应用程序/逻辑/用户.py
1 2 3 4 5 6 7 | 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)认识到用户逻辑实际上是用户模型。但由于这显然是一个黑客行为,我很高兴接受总是为
Django的设计目的是方便地用于交付网页。如果您对此不满意,也许您应该使用另一种解决方案。
我正在编写模型上的根或公共操作(具有相同的接口)以及模型控制器上的其他操作。如果我需要其他模型的操作,我就导入它的控制器。
这种方法对我和我的应用程序的复杂性来说已经足够了。
Hedde的响应就是一个例子,展示了Django和Python本身的灵活性。
不管怎样,这个问题很有趣!