首页 文章

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

提问于
浏览
363

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

这是困扰我的:

  • 我发现我的模型级别(它应该只负责处理数据库中的数据)也发送电子邮件,在API上运行到其他服务等等,我觉得很难看 .

  • 此外,我发现在业务逻辑中放置业务逻辑是不可接受的,因为这样就很难控制 . 例如,在我的应用程序中,至少有三种方法可以创建 User 的新实例,但从技术上讲,它应该统一创建它们 .

  • 我并不总是注意到我的模型的方法和属性何时变得不确定,何时会产生副作用 .

这是一个简单的例子 . 起初, 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])

我想要的是在我的代码中分离实体:

  • Entities of my database, database level: What contains my application?

  • Entities of my application, business logic level: What can make my application?

实现可以在Django中应用的这种方法有哪些好的做法?

8 回答

  • 54

    我通常在视图和模型之间实现服务层 . 这就像您的项目的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()
    

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

  • 0

    首先,Don't repeat yourself .

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

    Take a look at active projects

    • 更多人=更需要正确组织

    • django repository他们有一个简单的结构 .

    • pip repository他们有一个直接的目录结构 .

    • fabric repository也是一个值得关注的好人 .

    • 您可以将所有模型放在 yourapp/models/logicalgroup.py

    • 例如 UserGroup 和相关型号可以在 yourapp/models/users.py

    • 例如 PollQuestionAnswer ......可能会在 yourapp/models/polls.py 之下

    • yourapp/models/__init__.py 里面 __all__ 中加载你需要的东西

    More about MVC

    • 型号是您的数据

    • 这包括您的实际数据

    • 这还包括您的session / cookie / cache / fs / index数据

    • 用户与控制器交互以操纵模型

    • 这可以是API,也可以是保存/更新数据的视图

    • 可以使用 request.GET / request.POST 等进行调整

    • 也认为 pagingfiltering .

    • 数据更新视图

    • 模板获取数据并相应地格式化

    • API甚至没有模板也是视图的一部分;例如 tastypiepiston

    • 这也应该占中间件 .

    Take advantage of middleware / templatetags

    • 如果您需要为每个请求完成一些工作,中间件是一种可行的方法 .

    • 例如添加时间戳

    • 例如更新有关页面匹配的指标

    • 例如填充缓存

    • 如果您的代码片段始终重复出现以格式化对象,则模板标签很好 .

    • 例如活动标签/网址面包屑

    Take advantage of model managers

    • 创建 User 可以进入 UserManager(models.Manager) .
      实例的

    • gory详细信息应该在 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()
    

    Make use of forms where possible

    如果您有映射到模型的表单,则可以删除许多样板代码 . ModelForm documentation非常好 . 如果您有大量的自定义(或者有时为了更高级的用途而避免循环导入错误),那么从模型代码中分离表单代码可能会很好 .

    Use management commands when possible

    • 例如 yourapp/management/commands/createsuperuser.py

    • 例如 yourapp/management/commands/activateinbulk.py

    if you have business logic, you can separate it out

    • 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
    

    more about design patterns

    more about interface boundaries

    • 您想要使用的代码是否真的是模型的一部分? - > yourapp.models

    • 代码是业务逻辑的一部分吗? - > yourapp.vendor

    • 通用工具/库的代码部分是什么? - > yourapp.libs

    • 代码是业务逻辑的一部分库? - > yourapp.libs.vendoryourapp.vendor.libs

    • 这是一个很好的:你可以独立测试你的代码吗?

    • 是的,好:)

    • 不,您可能遇到界面问题

    • 当有明显的分离时,unittest应该是轻而易举的the use of mocking

    • 分离是否合乎逻辑?

    • 是的,好:)

    • 不,您可能无法单独测试这些逻辑概念 .

    • 当你获得10倍以上的代码时,你认为你需要重构吗?

    • 是的,没有好,没有bueno,重构可能是很多工作

    • 不,那太棒了!

    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

    或任何其他帮助你;找到 interfaces you needboundaries 会对你有所帮助 .

  • -3

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

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

    这种方法足以让我和我的应用程序的复杂性 .

    Hedde的回答是一个例子,展示了django和python本身的灵活性 .

    反正非常有趣的问题!

  • 0

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

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

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

  • 21

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

    在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),你可以制作例如用户配置文件,或者至少阅读它的代码,它对你的情况很有用 .

  • 5

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

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

  • 480

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

    此外,我已将您问题的第3部分解释为:如何注意未能将这些模型分开 .

    这是两个非常不同的概念,并且总是很难将它们分开 . 但是,有一些常用的模式和工具可用于此目的 .

    关于域模型

    您需要认识到的第一件事是您的域模型并不是真正的数据;它涉及的行动和问题,如"activate this user","deactivate this user","which users are currently activated?"和"what is this user's name?" . 用经典术语来说:它是关于查询和命令的 .

    在命令中思考

    让我们从查看示例中的命令开始:“激活此用户”和“停用此用户” . 关于命令的好处是它们可以通过小的给定时间场景来表达:

    当管理员激活此用户时,如果用户处于非活动状态,则用户将变为活动状态,并向用户发送确认电子邮件,并将一个条目添加到系统日志中(等等)

    这样的场景对于了解单个命令如何影响基础架构的不同部分(在这种情况下是您的数据库(某种“活动”标志),邮件服务器,系统日志等)非常有用 .

    这样的场景也真正帮助您 Build 测试驱动开发环境 .

    最后,在命令中思考确实可以帮助您创建面向任务的应用程序 . 您的用户会很感激:-)

    表达命令

    Django提供了两种表达命令的简单方法;它们都是有效的选择,混合这两种方法并不罕见 .

    服务层

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

    • 命令的执行(它做了什么?)

    • 验证命令参数(可以这样做吗?)

    • 命令的显示(我该怎么做?)

    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
    

    在查询中思考

    您的示例不包含任何查询,因此我冒昧地编写了一些有用的查询 . 我更喜欢使用术语“问题”,但查询是经典术语 . 有趣的查询是:“此用户的名称是什么?”,“此用户可以登录吗?”,“显示已停用的用户列表”和“已停用用户的地理分布是什么?”

    在着手回答这些问题之前,您应该总是问自己两个问题:这是一个仅针对我的模板的表示性查询,和/或与执行我的命令和/或报告查询相关的业务逻辑查询 .

    仅仅为了改进用户界面而进行表示性查询 . 业务逻辑查询的答案直接影响命令的执行 . 报告查询仅用于分析目的,并且具有更宽松的时间限制 . 这些类别并不相互排斥 .

    另一个问题是:“我能完全控制答案吗?”例如,在查询用户名时(在此上下文中),我们无法控制结果,因为我们依赖于外部API .

    进行查询

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

    User.objects.filter(active=True)
    

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

    自定义标签和过滤器

    第一种选择是对于仅仅是表示的查询非常有用:自定义标记和模板过滤器 .

    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
    

    代理模型

    代理模型在业务逻辑和报告的上下文中非常有用 . 您基本上定义了模型的增强子集 . 您可以通过覆盖Manager.get_queryset()方法来覆盖Manager的基本QuerySet .

    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
    

    查询模型

    对于本质上复杂但经常执行的查询,存在查询模型的可能性 . 查询模型是非规范化的一种形式,其中单个查询的相关数据存储在单独的模型中 . 当然,技巧是使非规范化模型与主模型保持同步 . 只有在完全由您控制的情况下才能使用查询模型 .

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

    更好的选择是使用自定义信号 . 这些信号当然是由您的命令发出的 . 信号的优势在于您可以使多个查询模型与原始模型保持同步 . 此外,可以使用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()
    

    保持清洁

    使用这种方法时,确定代码是否保持干净变得非常容易 . 请遵循以下准则:

    • 我的模型是否包含的方法不仅仅是管理数据库状态?你应该提取一个命令 .

    • 我的模型是否包含不映射到数据库字段的属性?您应该提取查询 .

    • 我的模型是否引用了不属于我的数据库的基础设施(例如邮件)?你应该提取一个命令 .

    视图也是如此(因为视图经常遇到同样的问题) .

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

    一些参考文献

    Django documentation: proxy models

    Django documentation: signals

    Architecture: Domain Driven Design

  • 115

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

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

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

    • 数据库定义仍然只是 - 没有附加逻辑"garbage"

    • 与模型相关的逻辑全部放在一个地方

    • 所有服务(表单,REST,视图)都有一个逻辑访问点

    • Best of all: 一旦我意识到我的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 ...
    

    我唯一能够接受 self 参数总是指定类型的小麻烦 .

相关问题