在Django中分离业务逻辑和数据访问

我正在Django写一个项目,我看到80%的代码在models.py文件models.py 。 这段代码令人困惑,在一段时间之后,我停止了解真正发生的事情。

这是困扰我的东西:

  1. 我发现我的模型级别(它应该只负责数据库中的数据)的工作也在发送电子邮件,在其他服务上发送API等。
  2. 另外,我觉得在视图中放置业务逻辑是不可接受的,因为这样很难控制。 例如,在我的应用程序中,至less有三种方法来创buildUser新实例,但从技术上讲,它应该一致地创build它们。
  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中应用这种方法?

    您似乎在询问数据模型领域模型之间的区别 – 后者是您可以在最终用户看到的业务逻辑和实体的位置,前者是您实际存储数据的地方。

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

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

    关于领域模型

    你需要认识的第一件事是你的领域模型不是关于数据的; 它是关于诸如“激活这个用户”,“closures这个用户”,“哪些用户当前被激活?”以及“这个用户的名字是什么?”等操作问题 。 经典的说法是关于查询命令

    在命令中思考

    我们先看看例子中的命令:“激活这个用户”和“closures这个用户”。 关于命令的好处是,它们可以很容易地用小时给定的场景来表示:

    一个不活跃的用户
    pipe理员激活这个用户
    那么用户变得活跃
    并向用户发送确认电子邮件
    并将一个条目添加到系统日志中
    (等等)

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

    这样的场景也真的帮助你build立一个testing驱动的开发环境。

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

    expression命令

    Django提供了两种expression命令的简单方法; 他们都是有效的select,混合这两种方法并不罕见。

    服务层

    服务模块已经被@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表单。 我更喜欢这种方法,因为它结合了多个密切相关的方面:

    • 命令的执行(它是干什么的?)
    • validation命令参数(可以这样做吗?)
    • 命令的介绍(我该怎么做?)

    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 

    在查询中思考

    你的例子不包含任何查询,所以我冒昧地组成了一些有用的查询。 我更喜欢使用术语“问题”,但查询是古典术语。 有趣的查询是:“这个用户的名字是什么?”,“这个用户可以login吗?”,“给我一个停用用户列表”和“停用用户的地理分布是什么?

    在开始回答这些查询之前,您应该总是问自己两个问题:这是仅供我的模板使用的表示式查询,和/或与执行我的命令和/或报告查询绑定的业务逻辑查询。

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

    另一个问题是:“我能完全控制答案吗?” 例如,当查询用户的名字(在这种情况下),我们不能控制结果,因为我们依靠外部的API。

    查询

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

     User.objects.filter(active=True) 

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

    自定义标签和filter

    第一个select对于仅仅是表示性的查询是有用的:自定义标签和模板filter。

    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 

    代理模型

    代理模型在业务逻辑和报表环境中非常有用。 你基本上定义了你的模型的增强子集。

    models.py

     class InactiveUserManager(models.Manager): def get_query_set(self): query_set = super(InactiveUserManager, self).get_query_set() 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 

    查询模型

    对于本质上复杂但频繁执行的查询,可能存在查询模型。 查询模型是非规范化的一种forms,其中单个查询的相关数据存储在单独的模型中。 当然诀窍是保持非规范化模型与主模型同步。 查询模型只能在完全受您控制的情况下使用。

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

    保持干净

    使用这种方法时,确定代码是否保持清洁变得非常容易。 只要遵循以下准则:

    • 我的模型是否包含比pipe理数据库状态更多的方法? 你应该提取一个命令。
    • 我的模型是否包含不映射到数据库字段的属性? 你应该提取一个查询。
    • 我的模型是否引用不是我的数据库的基础结构(如邮件)? 你应该提取一个命令。

    同样的观点(因为意见经常遭受同样的问题)。

    • 我的观点是否主动pipe理数据库模型? 你应该提取一个命令。

    一些参考

    Django文档:代理模型

    Django文档:信号

    架构:领域驱动devise

    我通常在视图和模型之间实现一个服务层。 这就像你的项目的API一样,给你一个很好的直升机视图。 我从我的一个同事那里inheritance了这个实践,这个实践使用了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 """ if limit: return Book.objects.filter(**filters)[:limit] return Book.objects.filter(**filters) 

    views.py

     from library.services import get_books class BookListView(ListView): """ simple view, eg implement a _build and _apply filters function """ queryset = get_books() 

    请注意,我通常会将模型,视图和服务提供到模块级别,并根据项目的大小进一步分离

    首先, 不要重复自己 。

    那么请小心,不要过度劳累,有时候只是浪费时间,让人失去重要的一面。 不时阅读python的禅宗 。

    看看活跃的项目

    • 更多的人=更需要妥善组织
    • 他们有一个简单的结构django存储库 。
    • 他们有一个straigtforward目录结构的点pipe理库 。
    • 面料库也是一个很好的看看。

      • 您可以将所有模型放置在yourapp/models/logicalgroup.py
    • 例如UserGroup和相关模型可以在yourapp/models/users.py
    • PollQuestionAnswer …可以根据yourapp/models/polls.py
    • yourapp/models/__init__.py中的__all__加载所需的yourapp/models/__init__.py

    更多关于MVC

    • 模型是你的数据
      • 这包括您的实际数据
      • 这还包括你的session / cookie / cache / fs / index数据
    • 用户与控制器交互操作模型
      • 这可能是一个API,或保存/更新您的数据的视图
      • 这可以通过request.GET / request.POST等来调整
      • 认为寻呼过滤
    • 数据更新视图
      • 模板将采取相应的数据并进行格式化
      • 甚至没有模板的API也是视图的一部分; 例如tastypiepiston
      • 这也应该考虑到中间件。

    利用中间件 / 模板标签

    • 如果您需要为每个请求完成一些工作,中间件是一条可行的路线。
      • 例如添加时间戳
      • 例如更新关于页面命中的度量
      • 例如填充caching
    • 如果你有代码片段总是重复的格式化对象,templatetags是好的。
      • 例如活动标签/url面包屑

    充分利用模特经理

    • 创buildUser可以进入UserManager(models.Manager)
    • 实例的细节应该放在models.Modelmodels.Model
    • queryset血统细节可以放在models.Manager
    • 您可能需要一次创build一个User ,因此您可能认为它应该位于模型本身上,但在创build该对象时,您可能不具备所有的细节:

    例:

     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相当不错。 如果您有很多定制(或者有时为了更高级的用途,有时候避免循环导入错误),将表单的代码从模型代码中分离出来可能会很好。

    尽可能使用pipe理命令

    • 例如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
    • 将困难的业务逻辑委托给后端
    • 确保在input/输出上设置期望权限。
    • 改变业务逻辑就像改变设置一样简单:)

    后端示例:

     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 

    更多关于devise模式

    • 关于devise模式已经有一个很好的问题了
    • 一个关于实际devise模式的非常好的video
    • Django的后端是明显使用委托devise模式。

    更多关于界面边界

    • 你想使用的代码是模型的一部分吗? – > yourapp.models
    • 代码是业务逻辑的一部分吗? – > yourapp.vendor
    • 是通用工具/库的代码部分? – > yourapp.libs
    • 代码是业务逻辑库的一部分吗? – > yourapp.libs.vendoryourapp.vendor.libs
    • 这是一个很好的方法:你能独立地testing你的代码吗?
      • 对很好 :)
      • 不,你可能有一个接口问题
      • 当有明确的分离时,unit testing应该是一个用嘲弄的风
    • 分离是合乎逻辑的吗?
      • 对很好 :)
      • 不,你可能无法单独testing这些逻辑概念。
    • 当你获得10倍以上的代码时,你认为你需要重构吗?
      • 是的,不好,没有bueno,重构可能是很多工作
      • 不,那真是太棒了!

    总之,你可以有

    • 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

    或其他任何可以帮助你的东西; find你需要接口边界将帮助你。

    Django采用了稍微改进的MVC。 Django中没有“控制器”的概念。 最接近的代理是一个“视图”,这往往会导致与MVC转换混淆,因为在MVC中,视图更像是Django的“模板”。

    在Django中,“模型”不仅仅是一个数据库抽象。 在某些方面,它与Django作为MVC的控制者的“观点”分担责任。 它拥有与一个实例相关的整个行为。 如果该实例需要与外部API交互作为其行为的一部分,那么这仍然是模型代码。 实际上,模型根本不需要与数据库进行交互,因此您可以设想具有完全作为外部API的交互层存在的模型。 这是一个“模型”更自由的概念。

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

    在Django中,MVC的实现方式如下:

    视图图层被分成两部分。 这些视图只能用于pipe理HTTP请求,它们被调用并响应它们。 视图与你的应用程序的其余部分(表单,模型,自定义类,在简单情况下直接与模型)进行通信。 要创build接口,我们使用模板。 模板类似于Django的string,它将一个上下文映射到它们中,并且这个上下文被应用程序传递给视图(当视图询问时)。

    模型层给出封装,抽象,validation,智能,并使你的数据面向对象(他们说有一天DBMS也会)。 这并不意味着你应该制作巨大的models.py文件(事实上,一个非常好的build议是在不同的文件中分割你的模型,把它们放到一个名为'models'的文件夹中,在这个文件中创build一个'__init__.py'文件您可以导入所有模型,最后使用models.Model类的属性“app_label”。 模型应该从数据操作中抽象出来,这会使你的应用程序变得更简单。 如果需要的话,还应该创build外部类,例如模型的“工具”。您还可以在模型中使用遗产,将模型的Meta类的“抽象”属性设置为“真”。

    其余的在哪里? 那么,小型Web应用程序通常是一种数据接口,在一些使用视图来查询或插入数据的小程序情况下就足够了。 更常见的情况是使用Forms或者ModelForms,它们实际上是“控制器”。 这不是对一个普遍问题的实际解决scheme,而是一个非常快速的解决scheme。 这是一个网站用来做什么。

    如果Forms不是为你编写的,那么你应该创build自己的类来做这个魔术,这是一个非常好的例子:pipe理应用程序:你可以读取ModelAmin代码,这实际上是一个控制器。 没有一个标准的结构,我build议你检查现有的Django应用程序,这取决于每个案例。 这就是Django开发人员所期望的,您可以添加xmlparsing器类,API连接器类,添加Celery执行任务,为基于reactor的应用程序扭曲,仅使用ORM,创buildWeb服务,修改pipe理应用程序等等。 ..这是你的责任,做出高质量的代码,尊重MVC的哲学,使它基于模块,并创build自己的抽象层。 这非常灵活。

    我的build议是:尽可能多地读取代码,周围有很多django应用程序,但不要太认真。 每个案例都是不同的,模式和理论有所帮助,但并不总是,这是一个不准确的科学,Django只是提供你很好的工具,你可以用来消除一些痛苦(如pipe理界面,Web表单validation,国际化,观察员模式实施,所有前面提到的和其他的),但好的devise来自经验丰富的devise师。

    PS:从auth应用程序(从标准的Django)使用'用户'类,你可以使例如用户configuration文件,或至less读取其代码,这将是有用的你的情况。

    我主要同意所选的答案( https://stackoverflow.com/a/12857584/871392 ),但要添加选项进行查询部分。

    我们可以为模型定义QuerySet类来进行过滤查询和子查询。 之后,您可以代理此模型的pipe理器的查询集类,如内置pipe理器和QuerySet类。

    尽pipe如果你不得不查询几个数据模型来获得一个域模型,我觉得把它放在一个单独的模块中似乎更合理一些,就像之前build议的那样。

    一个古老的问题,但我想提供我的解决scheme。 这是基于接受模型对象也需要一些额外的function,而把它放在models.py中是很尴尬的。 重商业逻辑可以根据个人的喜好分开编写,但我至less喜欢模型去做与自身有关的所有事情。 这个解决scheme也支持那些喜欢把所有的逻辑放在模型本身的人。

    因此,我devise了一个黑客机制 ,允许我将逻辑从模型定义中分离出来,并从我的IDE中获得所有暗示。

    优点应该是显而易见的,但是这里列出了我观察到的一些:

    • 数据库定义仍然是 – 没有逻辑“垃圾”附加
    • 模型相关的逻辑全部放置在一个地方
    • 所有的服务(表单,REST,视图)都有一个逻辑访问点
    • 最重要的是:一旦我意识到我的models.py变得杂乱无章,不得不将逻辑分离,我不必重写任何代码。 分离是平滑和迭代的:我可以在一个时间或整个课程或整个models.py中执行一个function。

    我一直使用Python 3.4以上和Django 1.8以上。

    应用程序/ models.py

     .... from app.logic.user import UserLogic class User(models.Model, UserLogic): field1 = models.AnyField(....) ... field definitions ... 

    应用程序/逻辑/ 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实际上是用户模型。 但是由于这显然是一个黑客,我很乐意接受总是指定types为self参数的小麻烦。

    Django被devise为可以轻松地用于传递网页。 如果你不舒适,也许你应该使用另一种解决scheme。

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

    这种方法对我来说已经足够了,我的应用程序也非常复杂。

    Hedde的回答是一个例子,显示了Django和Python本身的灵活性。

    非常有趣的问题无论如何!