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

Python django中业务逻辑和数据访问的分离,python,django,model-view-controller,data-access-layer,business-logic-layer,Python,Django,Model View Controller,Data Access Layer,Business Logic Layer,我正在用Django编写一个项目,我看到80%的代码都在文件models.py中。这段代码令人困惑,经过一段时间后,我不再理解真正发生的事情 以下是困扰我的问题: 我发现我的模型级别(应该是 仅负责处理数据库中的数据)也是 发送电子邮件、在API上漫游到其他服务等 此外,我发现在视图中放置业务逻辑是不可接受的,因为 这样就很难控制了。例如,在我的 应用程序创建新应用程序至少有三种方法 用户的实例,但从技术上讲,它应该统一创建它们 我并不总是注意到方法和 我的模型的属性在发展时变得不确定 副作用

我正在用Django编写一个项目,我看到80%的代码都在文件
models.py
中。这段代码令人困惑,经过一段时间后,我不再理解真正发生的事情

以下是困扰我的问题:

  • 我发现我的模型级别(应该是 仅负责处理数据库中的数据)也是 发送电子邮件、在API上漫游到其他服务等
  • 此外,我发现在视图中放置业务逻辑是不可接受的,因为 这样就很难控制了。例如,在我的 应用程序创建新应用程序至少有三种方法
    用户的实例
    ,但从技术上讲,它应该统一创建它们
  • 我并不总是注意到方法和 我的模型的属性在发展时变得不确定 副作用
  • 下面是一个简单的例子。起初,
    用户
    模型是这样的:

    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])
    
    我想要的是在我的代码中分离实体:

  • 我的数据库实体,持久性级别:我的应用程序保存哪些数据?
  • 我的应用程序的实体,业务逻辑级别:我的应用程序做什么?

  • 在Django中实现这种方法的最佳实践是什么?

    我通常在视图和模型之间实现一个服务层。这就像您的项目的API一样,可以让您从直升机上很好地了解正在发生的事情。我从我的一位同事那里继承了这种做法,他在Java项目(JSF)中大量使用这种分层技术,例如:

    型号.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
    
    from library.services import get_books
    
    class BookListView(ListView):
        """ simple view, e.g. implement a _build and _apply filters function """
        queryset = get_books()
    
    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
    
    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
    
    @register.filter
    def friendly_name(user):
        return remote_api.get_cached_name(user.id)
    
    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 
    
    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
    
    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()
    
    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)
    
    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()
        
    
    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)
    
    视图.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
    
    from library.services import get_books
    
    class BookListView(ListView):
        """ simple view, e.g. implement a _build and _apply filters function """
        queryset = get_books()
    
    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
    
    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
    
    @register.filter
    def friendly_name(user):
        return remote_api.get_cached_name(user.id)
    
    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 
    
    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
    
    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()
    
    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)
    
    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()
        
    
    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)
    
    请注意,我通常将模型、视图和服务带到模块级和 根据项目的规模进一步分离


    Django设计用于轻松交付网页。如果您对此不满意,也许您应该使用另一种解决方案

    我正在模型上编写根操作或公共操作(以具有相同的接口),以及模型控制器上的其他操作。如果我需要其他模型的操作,我将导入其控制器

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

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


    这个问题很有趣

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

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

    然后,请注意不要过度设计,有时这只是浪费时间,会让人失去对重要事情的关注。不时地回顾一下这篇文章

    查看活动项目

    • 更多的人=更需要合理组织
    • 它们有一个简单的结构
    • 它们有一个straigtforward目录结构
    • 这也是一个很好的一看

      • 您可以将所有模型放在应用程序/models/logicalgroup.py下
    • e、 g
      用户
      和相关模型可以放在
      你的应用程序/模型/用户.py下
    • e、 g
      投票
      问题
      答案
      。。。可以在你的app/models/polls.py下进入
    • 你的应用程序/模型/\uuuuu init\uuuuuuuuuuupy的
      中加载你需要的内容

    • 模型是您的数据
      • 这包括您的实际数据
      • 这还包括会话/cookie/cache/fs/index数据
    • 用户与控制器交互以操纵模型
      • 这可以是API,也可以是保存/更新数据的视图
      • 这可以通过
        request.GET
        /
        request.POST
        等进行调整
      • 思考分页过滤
    • 数据将更新视图
      • 模板获取数据并相应地格式化
      • 甚至不带模板的API都是视图的一部分;e、 g.
        tastype
        活塞
      • 这也应该考虑中间件
    利用/

    • 如果您需要为每个请求做一些工作,中间件是一种方法。
      • e、 g.添加时间戳
      • e、 g.更新有关页面点击率的指标
      • e、 g.填充缓存
    • 如果您有一些代码片段总是在格式化对象时重复出现,那么templatetags很好。
      • e、 g.活动选项卡/url面包屑
    利用

    • 创建
      User
      可以进入
      UserManager(models.Manager)
    • 实例的详细信息应该在
      models.Model
    • queryset
      的详细信息可以在
      models.Manager中找到
    • 您可能希望一次创建一个
      用户
      ,因此您可能认为它应该位于模型本身上,但在创建对象时,您可能不具备所有详细信息:
    例如:

    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()
    
    尽可能使用表格

    很多样板代码都可以使用elimi