Разделение бизнес-логики и доступа к данным в, Джанго

Я пишу проект в Django и я вижу, что 80% кода в файле models.py. Этот код сбивает с толку, и через некоторое время я перестаю понимать, что происходит на самом деле.

вот что меня беспокоит:

  1. Я считаю уродливым, что мой уровень модели (который должен был быть отвечает только за работу с данными из базы данных) тоже отправка электронной почты, прогулка по API другим сервисам и т. д.
  2. кроме того, я считаю недопустимым поместите бизнес-логику в представление, потому что таким образом, становится трудно контролировать. Например, в моей приложение существует как минимум три способа создания новых примеры User, но технически он должен создать их равномерно.
  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?

8 ответов


похоже, вы спрашиваете о разнице между модель данных и модель предметной области – последний, где вы можете найти бизнес-логику и сущности, как воспринимается вашим конечным пользователем, первый, где вы фактически храните свои данные.

кроме того, я интерпретировал 3-ю часть вашего вопроса как: как заметить неспособность держать эти модели отдельно.

это две очень разные концепции, и это всегда трудно держать их порознь. Однако есть некоторые общие шаблоны и инструменты, которые могут быть использованы для этой цели.

о модели домена

первое, что вам нужно признать, это то, что ваша модель домена на самом деле не о данных; это о действия и вопросы такие как "активировать пользователя", "отключить пользователя", "Какие пользователи в данный момент активирован?", и "как зовут этого пользователя?". В классических терминах: это о запросы и команды.

мышление в командах

давайте начнем с просмотра команд в вашем примере: "активировать этого пользователя"и" деактивировать этого пользователя". Хорошая вещь о командах заключается в том, что они могут быть легко выражены небольшим заданным сценарием:

дано неактивный пользователь
, когда администратор активирует этого пользователя
затем пользователь становится активным
и пользователю отправляется письмо с подтверждением
и в системный журнал добавляется запись
(п. так далее.)

такие сценарии полезны, чтобы увидеть, как различные части вашей инфраструктуры могут быть затронуты одной командой – в этом случае ваша база данных (какой-то "активный" флаг), ваш почтовый сервер, ваш системный журнал и т. д.

такой сценарий также действительно поможет вам в настройке тестовой среды разработки.

и, наконец, мышление в командах действительно помогает вам создать приложение, ориентированное на задачу. Ваши пользователи оценят это : -)

Команды, Выразив

Django предоставляет два простых способа выражения команд; они оба являются допустимыми вариантами, и нет ничего необычного в смешивании двух подходов.

уровень сервиса

на сервис модуль уже описано @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)

конечно, это работает только, если данные представлены в модели данных. Это не всегда так. В этих случаях вы можете рассмотреть следующие варианты.

пользовательские теги и фильтры

первая альтернатива полезна для запросов, которые являются просто презентационными: пользовательские теги и фильтры шаблонов.

шаблон.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() метод.

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

лучше опция будет использовать пользовательские сигналы. Эти сигналы, конечно, испускаются вашими командами. Сигналы имеют то преимущество, что можно синхронизировать несколько моделей запросов с исходной моделью. Кроме того, обработка сигналов может быть выгружена в фоновые задачи с использованием сельдерея или аналогичных фреймворков.

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

держать его в чистоте

при использовании этого подхода становится смешно легко определить, остается ли ваш код чистым. Просто следуйте этим рекомендациям:

  • содержит ли моя модель методы, которые делают больше, чем управление состоянием базы данных? Вы должны извлечь команду.
  • содержит ли моя модель свойства, которые не сопоставляются с полями базы данных? Вы должны извлечь запрос.
  • является ли моя модель ссылочной инфраструктурой, которая не является моей базой данных (например, mail)? Вы должны извлечь команду.

то же самое касается представлений (потому что представления часто страдают от одной и той же проблемы).

  • мой взгляд активно управляет моделями баз данных? Вы должны извлечь команду.

некоторые Ссылки

документация Django: прокси-модели

Django документация: сигналы

Архитектура: Домен Управляемый Дизайн


Я обычно реализую уровень обслуживания между представлениями и моделями. Это действует как API вашего проекта и дает вам хороший обзор вертолета того, что происходит. Я унаследовал эту практику от моего коллеги, который использует эту технику многослойности с Java-проектами (JSF), e.g:

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

имейте в виду, я обычно возьмите модели, представления и услуги на модульный уровень и отделить еще дальше в зависимости от размера проекта


прежде всего,не повторяйся.

тогда, пожалуйста, будьте осторожны, чтобы не перегружать, иногда это просто пустая трата времени, и заставляет кого-то потерять фокус на то, что важно. Обзор Дзен питона время от времени.

взгляните на активные проекты

  • больше людей = больше нужно правильно организовать
  • на репозиторий django у них есть простая структура.
  • на pip репозиторий они имеют структуру каталогов straigtforward.
  • the хранилище ткань тоже хорошо посмотреть.

    • вы можете разместить все свои модели под yourapp/models/logicalgroup.py
  • Эл.г User, Group и родственные модели могут идти под yourapp/models/users.py
  • Эл.г Poll, Question, Answer ... может yourapp/models/polls.py
  • загрузить то, что вам нужно в __all__ внутри yourapp/models/__init__.py

подробнее о MVC

  • модель данных
    • это включает в себя
    • это также включает в себя ваш сеанс / cookie / cache / fs / index data
  • потребитель взаимодействует с регулятором для того чтобы манипулировать модель
    • это может быть API или представление, которое сохраняет / обновляет ваши данные
    • это можно настроить с помощью request.GET / request.POST ...и т. д.
    • думаю пейджинговая или фильтрация тоже.
  • обновление данных в представлении
    • шаблоны берут данные и форматируют их соответственно
    • API даже без шаблонов являются частью представления; например tastypie или piston
    • это также должно учитывать промежуточное программное обеспечение.

воспользоваться промежуточное / templatetags

  • Если вам нужна работа для каждого запроса, middleware-это один из способов.
    • например, добавление метки
    • например, обновление метрик о посещениях страниц
    • например, заполнение кэша
  • если у вас есть фрагменты кода, которые всегда повторяются для форматирования объектов templatetags хороши.
    • например, активная вкладка / url хлебные крошки

воспользоваться менеджеры модель

  • создания User может перейти в UserManager(models.Manager).
  • кровавые детали для экземпляров должны идти на models.Model.
  • подробности на queryset может перейти в models.Manager.
  • возможно, вы захотите создать User один на a время, поэтому вы можете подумать, что он должен жить на самой модели, но при создании объекта у вас, вероятно, нет всех деталей:

пример:

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

использовать формы, где это возможно

много шаблонного кода можно исключить, если у вас есть формы, которые сопоставляются с моделью. The ModelForm documentation - это очень хорошо. Разделение кода для форм и кода модели может быть хорошим, если у вас много настроек (или иногда избегайте циклических ошибок импорта для более расширенного использования).

использовать команды управления когда можно

  • например 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
  • делегировать сложную бизнес-логику на сервер
  • убедитесь в том, чтобы установить ожидание на вход/выход.
  • изменение бизнес-логики так же просто, как изменения настроек :)

бэкэнд пример:

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

подробнее о шаблонах проектирования

подробнее о межфазные границы

  • - это код, который вы хотите использовать на самом деле часть моделей? -> yourapp.models
  • часть кода бизнес-логики? -> yourapp.vendor
  • является ли код частью generic tools / libs? -> yourapp.libs
  • является ли код частью бизнес-логики libs? -> yourapp.libs.vendor или yourapp.vendor.libs
  • вот хороший: можете ли вы проверить свой код самостоятельно?
  • является ли разделение логичным?
    • да, хорошая :)
    • нет, у вас могут возникнуть проблемы с тестированием этих логических концепций отдельно.
  • как вы думаете, вам нужно будет рефакторинг, когда вы получите 10x больше кода?
    • да, не хорошо, не хорошо, рефакторинг может быть много работы
    • нет, это просто потрясающе!

короче говоря, вы могли бы все остальное, что поможет вам; найти интерфейсы, которые вам нужны и границы поможет вам.


Django использует слегка измененный вид MVC. В Джанго нет понятия "контроллер". Ближайший прокси-сервер-это "представление", которое имеет тенденцию вызывать путаницу с преобразованиями MVC, потому что в MVC представление больше похоже на"шаблон" Django.

в Django "модель" - это не просто абстракция базы данных. В некоторых отношениях он разделяет обязанности с "видом" Django в качестве контроллера MVC. Он содержит все поведение, связанное с экземпляром. Если этот экземпляр должен взаимодействие с внешним API как часть его поведения, то это все равно модель код. Фактически, модели вообще не обязаны взаимодействовать с базой данных, поэтому вы можете представить себе модели, которые полностью существуют как интерактивный слой для внешнего API. Это гораздо более свободная концепция "модели".


в Django структура MVC, как сказал Крис Пратт, отличается от классической модели MVC, используемой в других фреймворках, я думаю, что основной причиной этого является избежание слишком строгой структуры приложения, как это происходит в других фреймворках MVC, таких как CakePHP.

в Django MVC был реализован следующим образом:

вид слой разделен на два. Представления должны использоваться только для управления HTTP-запросами, они вызываются и отвечают на них. Мнения связываются с остальные приложения (формы, modelforms, пользовательские классы, в простых случаях непосредственно с моделями). Для создания интерфейса мы используем шаблоны. Шаблоны строк-как в Джанго, он отображает контекст в них, и в этом контексте было направлено представление по заявлению (если просит).

Model layer дает инкапсуляцию, абстракцию, проверку, интеллект и делает ваши данные объектно-ориентированными (говорят, когда-нибудь СУБД также будет). Это не означает, что вы должны сделать огромные models.py файлы (на самом деле очень хороший совет-разделить ваши модели в разных файлах, поместить их в папку под названием "модели", сделать "__init__.py "файл в эту папку, где вы импортируете все свои модели и, наконец, используете атрибут "app_label" моделей.Класс моделей.) Модель должна абстрагировать вас от работы с данными, это сделает ваше приложение проще. При необходимости следует также создавать внешние классы, например "инструменты"для моделей.Вы также можете использовать heritage в моделях, устанавливая "абстрактный" атрибут мета-класса вашей модели для "True".

где остальные? Ну, небольшие веб-приложения, как правило, являются своего рода интерфейсом к данным, в некоторых небольших программных случаях достаточно использовать представления для запроса или вставки данных. Более распространенные случаи будут использовать формы или ModelForms, которые на самом деле являются"контроллерами". Это не что иное, как практическое решение общей проблемы, причем очень быстрое. Это то, что использует веб-сайт.

Если формы не enogh для вас, затем вы должны создать свои собственные классы, чтобы сделать магию, очень хорошим примером этого является приложение admin: вы можете читать код ModelAmin, это на самом деле работает как контроллер. Нет стандартной структуры, я предлагаю вам изучить существующие приложения Django, это зависит от каждого случая. Это то, что разработчики Django намеревались, вы можете добавить класс синтаксического анализатора xml, класс соединителя API, добавить сельдерей для выполнения задач, скрученный для реакторного приложения, использовать только ORM, сделать веб-службу, изменить приложение администратора и многое другое... Это ваша ответственность, чтобы сделать код хорошего качества, уважать философию MVC или нет, сделать его модульным и создать свои собственные слои абстракции. Он очень гибкий.

мой совет: прочитайте столько кода, сколько сможете, вокруг много приложений django, но не воспринимайте их так серьезно. Каждый случай отличается, шаблоны и теория помогают, но не всегда, это неточный опыт, django просто предоставляет вам хорошие инструменты, которые вы можете использовать для оживления некоторых боли (например, интерфейс администратора, проверка веб-формы, i18n, реализация шаблона наблюдателя, все ранее упомянутые и другие), но хорошие проекты исходят от опытных дизайнеров.

PS.: используйте класс "User" из приложения auth (из стандартного django), вы можете сделать, например, профили пользователей или, по крайней мере, прочитать его код, это будет полезно для вашего случая.


Я в основном согласен с выбранным ответом (https://stackoverflow.com/a/12857584/871392), но хотите добавить опцию в разделе "Создание запросов".

можно определить классы QuerySet для моделей для запросов фильтра и son on. После этого вы можете прокси-сервер этого класса queryset для менеджера модели, как это делают классы build-in Manager и QuerySet.

хотя, если вам пришлось запросить несколько моделей данных, чтобы получить одну модель домена, мне кажется более разумным поставить это в отдельном модуле, как предлагалось ранее.


старый вопрос, но я все равно хотел бы предложить свое решение. Он основан на принятии того, что объекты модели также требуют некоторой дополнительной функциональности, в то время как неудобно размещать ее в models.py. Тяжелая бизнес-логика может быть написана отдельно в зависимости от личного вкуса, но мне хотя бы нравится модель делать все, что связано с собой. Это решение также поддерживает тех, кто хотел бы иметь всю логику, размещенную в самих моделях.

таким образом, я придумано взлом это позволяет мне отделить логику от определений моделей и по-прежнему получать все намеки от моей IDE.

преимущества должны быть очевидны, но это перечисляет несколько, которые я заметил:

  • определения DB остаются только что-нет логики "мусор" прилагается
  • логика, связанная с моделью, аккуратно размещена в одном месте
  • все службы (формы, отдых, представления) имеют одну точку доступа к логика!--15-->
  • самое главное: мне не пришлось переписывать код, как только я понял, что мой models.py стал слишком загроможден и должен был отделить логику. Разделение гладкое и итеративное: я мог бы выполнять функцию за раз или весь класс или весь класс models.py.

я использовал это с Python 3.4 и выше и Django 1.8 и более значительный.

app/models.py

....
from app.logic.user import UserLogic

class User(models.Model, UserLogic):
    field1 = models.AnyField(....)
    ... field definitions ...

app/logic/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 на самом деле является моделью пользователя. Но поскольку это, очевидно, Хак, я очень рад принять небольшую неприятность всегда указывать тип для


Django предназначен для простого использования для доставки веб-страниц. Если вы не комфортно с этим, возможно, вы должны использовать другое решение.

Я пишу корневые или общие операции на модели (чтобы иметь тот же интерфейс) и другие на контроллере модели. Если мне нужна операция из другой модели я импортирую свой контроллер.

такого подхода мне достаточно и сложности моих приложений.

Hedde-пример это показывает гибкость самого django и python.

очень интересный вопрос в любом случае!