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

时间:2012-09-25 08:23:07

标签: python django model-view-controller data-access-layer business-logic-layer

我正在Django中编写一个项目,我发现80%的代码都在文件models.py中。这段代码令人困惑,经过一段时间后,我不再明白究竟发生了什么。

这让我感到困扰:

  1. 我觉得我的模型水平很丑陋(应该是这样的 仅负责处理来自数据库的数据) 发送电子邮件,使用API​​处理其他服务等。
  2. 另外,我发现在视图中放置业务逻辑是不可接受的,因为 这样就很难控制。例如,在我的 应用程序至少有三种创建新方法 User的实例,但从技术上讲,它应该统一创建它们。
  3. 我并不总是注意到方法和时间 我的模型的属性变得不确定,当它们发展时 副作用。
  4. 这是一个简单的例子。起初,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. 我的应用程序的实体,业务逻辑级别:什么可以使我的应用程序?
    3. 实现可以在Django中应用的这种方法有哪些好的做法?

9 个答案:

答案 0 :(得分:127)

我通常在视图和模型之间实现服务层。这就像您的项目的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 """
    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, e.g. implement a _build and _apply filters function """
    queryset = get_books()
  

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

答案 1 :(得分:60)

首先,Don't repeat yourself

然后,请注意不要过度工程,有时这只是浪费时间,让人失去对重要事项的关注。不时查看zen of python

查看有效项目

  • 更多人=更需要正确组织
  • django repository他们有一个简单的结构。
  • pip repository他们有一个直截了当的目录结构。
  • fabric repository也是一个很好的选择。

    • 您可以将所有模型放在yourapp/models/logicalgroup.py
  • 例如UserGroup和相关模型可以归yourapp/models/users.py
  • 例如PollQuestionAnswer ...可以归yourapp/models/polls.py
  • __all__
  • yourapp/models/__init__.py内加载您需要的内容

More about MVC

  • 模型是您的数据
    • 这包括您的实际数据
    • 这还包括您的session / cookie / cache / fs / index数据
  • 用户与控制器交互以操纵模型
    • 这可以是API,也可以是保存/更新数据的视图
    • 可以使用request.GET / request.POST ...等
    • 进行调整
    • 认为分页过滤
  • 数据更新视图
    • 模板获取数据并相应地格式化
    • API甚至没有模板也是视图的一部分;例如tastypiepiston
    • 这也应该占中间件。

利用middleware / templatetags

  • 如果您需要为每个请求完成一些工作,中间件是一种可行的方法。
    • e.g。添加时间戳
    • e.g。更新有关网页点击的指标
    • e.g。填充缓存
  • 如果您的代码片段始终重复出现以格式化对象,则模板标记很好。
    • e.g。活动标签/网址面包屑

利用model managers

  • 创建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非常好。如果您有大量的自定义(或者有时为了更高级的用途而避免循环导入错误),那么从模型代码中分离表单代码可能会很好。

尽可能使用management commands

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

如果您有业务逻辑,则可以将其分开

  • django.contrib.auth uses backends,就像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

有关设计模式的更多信息

有关界面边界的更多信息

  • 您想要使用的代码是否真的是模型的一部分? - &GT; yourapp.models
  • 代码是业务逻辑的一部分吗? - &GT; yourapp.vendor
  • 代码是通用工具/库的一部分吗? - &GT; yourapp.libs
  • 代码是业务逻辑库的一部分吗? - &GT; yourapp.libs.vendoryourapp.vendor.libs
  • 这是一个很好的:你能独立测试你的代码吗?
    • 是的,好的:)
    • 不,您可能遇到界面问题
    • 当有明确的分离时,单元测试应该是the use of mocking
    • 的一件轻而易举的事
  • 分离是否合乎逻辑?
    • 是的,好的:)
    • 不,您可能无法单独测试这些逻辑概念。
  • 当你获得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

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

答案 2 :(得分:24)

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

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

答案 3 :(得分:5)

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

在Django中,MVC以下列方式实现:

视图图层分为两部分。视图应仅用于管理HTTP请求,它们被调用并响应它们。视图与应用程序的其余部分(表单,模型,自定义类,简单情况下直接与模型)进行通信。 要创建界面,我们使用模板。模板与Django类似,它将上下文映射到它们中,并且应用程序将此上下文传递给视图(当视图请求时)。

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

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

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

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

PS:使用auth应用程序中的'User'类(来自标准django),你可以制作例如用户配置文件,或者至少阅读它的代码,它对你的情况很有用。

答案 4 :(得分:0)

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

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

虽然,如果你不得不查询几个数据模型来获得一个域模型,那么把它放在像之前建议的单独模块中似乎更合理。

答案 5 :(得分:0)

我必须同意你的看法。 django有很多可能性,但是最好的起点是查看Django's design philosophy

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

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

  

包括所有相关的域逻辑

     

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

  1. 您所描述的副作用是显而易见的,这里的逻辑可以更好地分解为Queryset和管理器。这是一个示例:

    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)
    

答案 6 :(得分:0)

关于优缺点的各种选择的最全面的文章:

  1. 想法1:胖子模型
  2. 观点2:将业务逻辑放入视图/表单
  3. 想法3:服务
  4. 想法4:查询集/管理器
  5. 结论

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

答案 7 :(得分:-1)

一个老问题,但无论如何我想提供我的解决方案。它基于接受模型对象也需要一些额外的功能,而将它放在 models.py 中是不方便的。繁重的商业逻辑可以根据个人品味单独编写,但我至少喜欢模型来做与自身相关的所有事情。该解决方案还支持那些喜欢将所有逻辑放在模型中的人。

因此,我设计了 hack ,它允许我将逻辑与模型定义分开,并且仍然可以从我的IDE中获得所有暗示。

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

  • 数据库定义仍然只是 - 没有附加逻辑“垃圾”
  • 与模型相关的逻辑全部放在一个地方
  • 所有服务(表单,REST,视图)都有一个逻辑
  • 访问点
  • 最重要的是:一旦我意识到我的 models.py 变得过于混乱并且不得不将逻辑分开,我就不必重写任何代码。分离是顺利和迭代的:我可以一次完成一个函数或整个类或整个models.py。

我一直在使用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实际上是用户模型。但由于这显然是一个黑客攻击,我很高兴接受总是为self参数指定类型的麻烦。

答案 8 :(得分:-3)

Django旨在轻松用于提供网页。如果您对此不满意,也许您应该使用另一种解决方案。

我正在模型的根目录或常用操作(具有相同的接口)和模型的控制器上的其他操作。如果我需要其他模型的操作,我导入它的控制器。

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

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

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