Поділ ділової логіки та доступу до даних у джанго


484

Я пишу проект у 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?


14
Читайте про сигнали
Костянтин

1
добре ви видалили тег, але ви можете використовувати DCI, щоб підтвердити роздільність того, що робить система (функціональність) і що система (модель даних / домен)
Rune FS,

2
Ви пропонуєте реалізувати всю логіку бізнесу в зворотному сигналі? На жаль, не всі мої програми можуть бути пов'язані з подіями в базі даних.
дефуз

Rune FS, я намагався використовувати DCI, але мені здавалося, що це не потрібно для мого проекту: контекст, визначення ролей як змішаних об'єктів тощо. Існує простіший спосіб розділення "робить" і " є "? Чи можете ви навести мінімальний приклад?
дефуз

Відповіді:


634

Схоже, ви запитуєте про різницю між моделлю даних та доменною моделлю - остання - це місце, де ви можете знайти ділову логіку та сутність, як сприймає ваш кінцевий користувач, перший - де ви фактично зберігаєте свої дані.

Крім того, я розтлумачив 3-ю частину вашого запитання як: як помітити невдачу утримувати ці моделі окремо.

Це дві дуже різні концепції, і завжди важко тримати їх окремо. Однак є деякі загальні закономірності та інструменти, які можна використовувати для цієї мети.

Про модель домену

Перше, що вам потрібно визнати, це те, що ваша модель домену насправді не стосується даних; йдеться про дії та питання, такі як "активувати цього користувача", "деактивувати цього користувача", "яких користувачів зараз активовано?" та "що це ім'я цього користувача?". У класичному плані: мова йде про запити та команди .

Мислення в командах

Для початку поглянемо на команди у вашому прикладі: "активувати цього користувача" та "відключити цього користувача". Приємна річ у командах полягає в тому, що вони можуть бути легко виражені малими сценаріями "задано коли-то":

якщо неактивний користувач,
коли адміністратор активує цього користувача,
він стає активним,
і електронною поштою підтвердження надсилається користувачеві,
а запис запису додається до системного журналу
(тощо. тощо)

Такі сценарії корисні для того, щоб побачити, як однією командою можуть впливати різні частини вашої інфраструктури - у цьому випадку ваша база даних (якийсь "активний" прапор), ваш поштовий сервер, ваш системний журнал тощо.

Такий сценарій також дуже допоможе вам створити середовище розробки тестових програм.

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

Вираження команд

Джанго надає два простих способи вираження команд; обидва вони є дійсними варіантами, і це незвично поєднувати два підходи.

Сервісний рівень

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

Використання форм

Інший спосіб - використовувати Форму Джанго для кожної команди. Я віддаю перевагу такому підходу, оскільки він поєднує в собі декілька тісно пов'язаних аспектів:

  • виконання команди (що вона робить?)
  • перевірка параметрів команди (чи може це зробити?)
  • презентація команди (як це зробити?)

form.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 

Проксі-моделі

Моделі проксі дуже корисні в контексті ділової логіки та звітності. Ви в основному визначаєте розширений підмножина вашої моделі. Ви можете замінити базовий QuerySet менеджера, замінивши 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)

Перший варіант - оновити ці моделі у своїх командах. Це дуже корисно, якщо ці моделі змінюються лише однією або двома командами.

form.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'])

form.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: моделі проксі

Документація Джанго: сигнали

Архітектура: Дизайн, керований доменом


11
Приємно бачити відповідь, що включає DDD у питання, пов'язані з джанго. Тільки тому, що Джанго використовує ActiveRecord для наполегливості, це не означає, що розділення проблем повинно вийти у вікно. Чудова відповідь.
Скотт Коутс

6
Якщо я хочу перевірити, що розроблений користувач є власником об'єкта перед тим, як видалити його, чи слід перевірити це у представленні або у формі / модулі обслуговування?
Іван

6
@Ivan: обидва. Він повинен бути у формі / модулі обслуговування, оскільки це частина ваших бізнес-обмежень. Це також має бути на увазі, оскільки ви повинні представляти лише ті дії, які користувачі можуть реально виконувати.
publysher

4
Призначений для користувача менеджер методу є хорошим способом для реалізації запитів: User.objects.inactive_users(). Але приклад моделі проксі тут IMO призводить до неправильної семантики: u = InactiveUser.objects.all()[0]; u.active = True; u.save()і все ж isinstance(u, InactiveUser) == True. Також я б зазначив, що ефективний спосіб підтримати модель запитів у багатьох випадках - це перегляд db.
Ар’є Лейб Таурог

1
@adnanmuttaleb Це правильно. Зауважте, що у самій відповіді використовується лише термін "Модель домену". Я включив посилання на DDD не тому, що моя відповідь - DDD, а тому, що ця книга чудово допомагає вам думати про доменні моделі.
publysher

148

Зазвичай я реалізую сервісний рівень між переглядами та моделями. Це працює як 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 """
    return Book.objects.filter(**filters)[:limit]  # list[:None] will return the entire list

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

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


8
Мені подобається загальний підхід, хоча, наскільки я розумію, ваш конкретний приклад, як правило, реалізовується як менеджер .
арі

9
@arie не обов'язково, мабуть, кращий приклад, коли послуги веб-магазину включатимуть такі речі, як генерування сеансів на кошиках, асинхронні завдання, такі як обчислення рейтингів продуктів, створення та надсилання електронних листів etcetera
Hedde van der Heide

4
Мені також подобається такий підхід, який також надходить ява. Я новачок у python, як би ви протестували views.py? Як би ви знущалися з рівня обслуговування (якщо, наприклад, сервіс здійснює деякі віддалені дзвінки api)?
Теймураз

71

Перш за все, не повторюйте себе .

Тоді, будь ласка, будьте обережні, щоб не перенапружуватись, іноді це просто марна трата часу, і змушує когось втратити увагу на важливому. Час від часу переглядайте дзен пітона .

Погляньте на активні проекти

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

    • ви можете розмістити всі свої моделі під yourapp/models/logicalgroup.py
  • наприклад User, Groupі пов'язані моделі можуть підходитиyourapp/models/users.py
  • наприклад Poll, Question, Answer... може піти підyourapp/models/polls.py
  • завантажте все, що потрібно __all__всерединіyourapp/models/__init__.py

Більше про MVC

  • модель - ваші дані
    • це включає ваші фактичні дані
    • сюди також входять дані сеансу / cookie / кеш / fs / індекс
  • Користувач взаємодіє з контролером для маніпулювання моделлю
    • це може бути API чи перегляд, який зберігає / оновлює ваші дані
    • це може бути налаштоване з request.GET/ request.POST... і т.д.
    • подумайте також про підкачку або фільтрування .
  • дані оновлюють представлення даних
    • шаблони беруть дані та форматують їх відповідно
    • API навіть без шаблонів є частиною перегляду; наприклад tastypieабоpiston
    • це також повинно враховувати проміжне програмне забезпечення.

Скористайтеся проміжними програмами / тегами шаблонів

  • Якщо вам потрібно виконати певну роботу для кожного запиту, проміжне програмне забезпечення - це один із способів.
    • наприклад, додавання міток часу
    • наприклад, оновлення показників про звернення сторінок
    • наприклад, заповнення кешу
  • Якщо у вас є фрагменти коду, які завжди повторюються для форматування об'єктів, шаблони тегів є хорошими.
    • наприклад, активні сухарі / URL-адреси

Скористайтеся менеджерами моделей

  • створення Userможе йти в a UserManager(models.Manager).
  • gory деталі для примірників повинні надходити на models.Model.
  • gory деталі для querysetмогли піти в a 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досить добре. Відокремлення коду для форм від модельного коду може бути корисним, якщо ви маєте багато налаштувань (або іноді уникаєте помилок циклічного імпорту для більш розширеного використання).

Використовуйте команди управління, коли це можливо

  • напр 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
  • Чи є частиною коду загальних інструментів / ліб? ->yourapp.libs
  • Чи є кодом частиною ділової логіки? -> yourapp.libs.vendorабоyourapp.vendor.libs
  • Ось хороший варіант: чи можете ви протестувати свій код самостійно?
    • так добре :)
    • ні, у вас може виникнути проблема з інтерфейсом
    • коли є чітке розділення, одиничним тестом повинен бути вітер із застосуванням глузування
  • Чи розділення логічне?
    • так добре :)
    • ні, у вас можуть виникнути проблеми з тестуванням цих логічних понять окремо.
  • Як ви думаєте, вам знадобиться рефактор, коли ви отримаєте на 10 разів більше коду?
    • так, ні добра, ні буено, рефактор може бути багато роботи
    • ні, це просто приголомшливо!

Коротше кажучи, можна було б

  • 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

або будь-що інше, що вам допомагає; знаходження потрібних інтерфейсів і меж допоможуть вам.


27

У Джанго використовується дещо модифікований вид MVC. У Джанго немає поняття "контролер". Найближчий проксі - це "погляд", який, як правило, викликає плутанину з перетвореннями MVC, оскільки в MVC подання більше нагадує "шаблон" Джанго.

У Джанго "модель" - це не просто абстрагування бази даних. У деяких аспектах він поділяє обов'язок з "поглядом" Джанго як на контролера MVC. Він містить всю поведінку, пов'язану з екземпляром. Якщо цьому екземпляру необхідно взаємодіяти із зовнішнім API як частина його поведінки, то це все ще код моделі. Насправді, моделям взагалі не потрібно взаємодіяти з базою даних, тому можна уявити моделі, які повністю існують як інтерактивний рівень для зовнішнього API. Це набагато вільніше поняття "моделі".


7

У Джанго структура MVC, як сказав Кріс Пратт, відрізняється від класичної моделі MVC, що використовується в інших структурах, я думаю, що головна причина цього - уникати занадто суворої структури додатків, як це відбувається в інших структурах MVC, таких як CakePHP.

У Джанго MVC було реалізовано таким чином:

Шар перегляду розділений на два. Перегляди повинні використовуватися лише для управління HTTP-запитами, вони викликаються та відповідають на них. Перегляди спілкуються з рештою вашої програми (форми, форми моделей, спеціальні класи, у простих випадках безпосередньо з моделями). Для створення інтерфейсу ми використовуємо Шаблони. Шаблони є схожими на Django, він відображає в них контекст, і цей контекст передається програмою для перегляду (коли запит запитує).

Модельний рівень дає інкапсуляцію, абстрагування, валідацію, інтелект та робить ваші дані об'єктно-орієнтованими (вони кажуть, що колись також будуть і СУБД). Це не означає, що ви повинні створювати величезні файли models.py (насправді дуже хороша порада - розділити свої моделі на різні файли, помістити їх у папку під назвою "models", внести файл "__init__.py" у цей файл папку, куди ви імпортуєте всі свої моделі та нарешті використовуєте атрибут 'app_label' моделей. Клас Модель). Модель повинна абстрагувати вас від роботи з даними, це зробить ваш додаток більш простим. Ви також повинні створити зовнішні класи, наприклад, "інструменти" для своїх моделей. Ви також можете використовувати спадщину в моделях, встановивши атрибут "абстрактного" мета-класу вашої моделі на "Істинно".

Де решта? Ну, невеликі веб-додатки, як правило, є свого роду інтерфейсом для даних, у деяких невеликих програмних випадках, достатньо використовувати подання для запиту чи вставки даних. Більш поширені випадки будуть використовувати Forms або ModelForms, які насправді є "контролерами". Це не що інше, як практичне рішення загальної проблеми, і дуже швидке. Це те, що використовується веб-сайт.

Якщо Форми для вас не є однозначними, то вам слід створити власні класи, щоб зробити магію, дуже хорошим прикладом цього є додаток адміністратора: ви можете прочитати код ModelAmin, він фактично працює як контролер. Немає стандартної структури, я пропоную вам вивчити наявні програми Django, це залежить від кожного конкретного випадку. Це те, що задумали розробники Django, ви можете додати клас аналізу parser XML, клас роз'єму API, додати Celery для виконання завдань, скручений для реакторної програми, використовувати тільки ORM, зробити веб-сервіс, змінити додаток адміністратора тощо. .. Ваша відповідальність робити якісний код, дотримуватися філософії MVC чи ні, робити його на основі модулів і створювати власні шари абстракції. Це дуже гнучко.

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

ПС .: використовуйте клас "Користувач" з програми auth (зі стандартного django), ви можете зробити, наприклад, профілі користувачів або хоча б прочитати його код, це буде корисно для вашого випадку.


1

Старе питання, але я хотів би запропонувати своє рішення все одно. Це засноване на визнанні того, що об'єкти моделі теж вимагають деякої додаткової функціональності, хоча її незручно розміщувати в межах models.py . Важка бізнес-логіка може бути написана окремо залежно від особистого смаку, але мені принаймні подобається, щоб модель робила все, що стосується самої себе. Це рішення також підтримує тих, хто любить всю логіку розміщувати в самих моделях.

Як такий, я розробив хак, який дозволяє мені відокремити логіку від визначень моделі і все ще отримати всі натяки від мого IDE.

Переваги повинні бути очевидними, але в цьому перелічено декілька речей, які я спостерігав:

  • Визначення БД залишаються саме цим - жодна логіка "сміття" не додається
  • Логіка, пов'язана з моделлю, розміщена акуратно в одному місці
  • Усі сервіси (форми, REST, перегляди) мають єдину точку доступу до логіки
  • Найкраще: мені не довелося переписувати жоден код, як тільки я зрозумів, що мій models.py став занадто захаращеним і довелося відокремлювати логіку. Розмежування є плавним та ітераційним: я могла виконувати функцію за один раз або весь клас або весь model.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 ...

додаток / логіка / 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параметра.


Насправді я бачу це як простий у використанні підхід. Але я б перемістив остаточну модель в інший файл, а не успадкував у models.py. Було б як сервіс.py були зіткненнями userlogic + модель
Макс,

1

Я мав би погодитися з вами. У джанго є багато можливостей, але найкраще почати - це переглянути філософію дизайну Django .

  1. Викликати API з властивості моделі не було б ідеально, здається, було б більше сенсу зробити щось подібне у поданні та, можливо, створити сервісний шар, щоб зберегти речі сухими. Якщо виклик до API не блокує, а виклик - дорогий, надсилання запиту сервісному працівнику (працівнику, який споживає з черги) може мати сенс.

  2. Відповідно до моделей філософії дизайну Джанго, капсулюється кожен аспект "об'єкта". Отже, вся бізнес-логіка, пов'язана з цим об'єктом, повинна жити там:

Включіть всю відповідну логіку домену

Моделі повинні інкапсулювати кожен аспект «об’єкта», слідуючи схемі дизайну Мартіна Фаулера «Active Record».

  1. Побічні ефекти, які ви описуєте, очевидні, логіка тут може бути краще розділена на запити та менеджери. Ось приклад:

    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)

0

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

Можна визначити класи QuerySet для моделей для створення запитів фільтрів тощо. Після цього ви можете проксі цей клас запитів для менеджера моделі, як це роблять вбудований диспетчер та класи QuerySet.

Хоча, якщо вам довелося запитати кілька моделей даних, щоб отримати одну модель домену, мені здається більш розумним розмістити це в окремому модулі, як пропонувалося раніше.


0

Найбільш вичерпна стаття про різні варіанти плюсів і мінусів:

  1. Ідея №1: Жирні моделі
  2. Ідея №2: Введення ділової логіки у перегляди / форми
  3. Ідея №3: Послуги
  4. Ідея №4: набори запитів / менеджери
  5. Висновок

Джерело: https://sunscrapers.com/blog/where-to-put-business-logic-django/


Вам слід додати пояснення.
m02ph3u5

-6

Django призначений для легкого використання для доставки веб-сторінок. Якщо вам це не подобається, можливо, вам слід скористатися іншим рішенням.

Я записую кореневі або загальні операції на моделі (щоб мати той самий інтерфейс), а інші - на контролері моделі. Якщо мені потрібна операція з іншої моделі, я імпортую її контролер.

Такого підходу мені достатньо і складності моїх заявок.

Відповідь Хедде - приклад, який показує гнучкість джанго та пітона.

Дуже цікаве питання все одно!


9
Як сказати, що це достатньо добре, щоб ви корисно зрозуміли моє питання?
Кріс Вессілінг

1
Django може запропонувати багато іншого, крім django.db.models, але більша частина екосистеми сильно залежить від вашої моделі, використовуючи моделі django.
andho

1
Шаблон дизайну, що використовується для розробки програмного забезпечення. І джанго призначений для легкого використання для доставки програмного забезпечення в середньому чи великому масштабі не лише веб-сторінок!
Мохаммед Торкашванд
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.