Rozdzielenie logiki biznesowej i dostępu do danych w django

484

Piszę projekt w Django i widzę, że 80% kodu znajduje się w pliku models.py. Ten kod jest mylący i po pewnym czasie przestaję rozumieć, co się naprawdę dzieje.

Oto, co mnie niepokoi:

  1. Uważam za brzydkie, że mój poziom modelu (który miał być odpowiedzialny tylko za pracę z danymi z bazy danych) również wysyła wiadomości e-mail, chodzi po interfejsie API do innych usług itp.
  2. Ponadto uważam za niedopuszczalne umieszczanie logiki biznesowej w widoku, ponieważ w ten sposób trudno jest kontrolować. Na przykład w mojej aplikacji istnieją co najmniej trzy sposoby tworzenia nowych instancji User, ale technicznie powinno się je tworzyć jednolicie.
  3. Nie zawsze zauważam, kiedy metody i właściwości moich modeli stają się niedeterministyczne i kiedy wywołują skutki uboczne.

Oto prosty przykład. Na początku Usermodel był taki:

class User(db.Models):

    def get_present_name(self):
        return self.name or 'Anonymous'

    def activate(self):
        self.status = 'activated'
        self.save()

Z czasem zmieniło się to w:

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

Chcę oddzielić jednostki w moim kodzie:

  1. Podmioty mojej bazy danych, poziom bazy danych: Co zawiera moją aplikację?
  2. Podmioty mojej aplikacji, poziom logiki biznesowej: Co może składać moją aplikację?

Jakie są dobre praktyki wdrażania takiego podejścia, które można zastosować w Django?

defuz
źródło
14
Przeczytaj o sygnałach
Konstant,
1
dobrze usunąłeś tag, ale możesz użyć DCI, aby uzyskać oddzielenie tego, co robi system (funkcjonalność) i czym jest system (model danych / domeny)
Rune FS
2
Czy chcesz zaimplementować całą logikę biznesową w sygnałach zwrotnych? Niestety nie cała moja aplikacja może być powiązana ze zdarzeniami w bazie danych.
defuz
Rune FS, próbowałem użyć DCI, ale wydawało mi się, że nie potrzebuje wiele dla mojego projektu: kontekst, definicja ról jako miksowanie z obiektami itp. Istnieje łatwiejszy sposób rozdzielenia „robi” i „ jest"? Czy możesz podać minimalny przykład?
defuz

Odpowiedzi:

634

Wygląda na to, że pytasz o różnicę między modelem danych a modelem domeny - w tym drugim można znaleźć logikę biznesową i podmioty postrzegane przez użytkownika końcowego, w tym drugim miejscu faktycznie przechowujesz swoje dane.

Ponadto zinterpretowałem trzecią część twojego pytania jako: jak zauważyć brak oddzielenia tych modeli.

Są to dwie bardzo różne koncepcje i zawsze trudno jest je rozdzielić. Istnieją jednak pewne typowe wzorce i narzędzia, które można wykorzystać w tym celu.

O modelu domeny

Pierwszą rzeczą, którą musisz rozpoznać, jest to, że twój model domeny tak naprawdę nie dotyczy danych; chodzi o działania i pytania, takie jak „aktywuj tego użytkownika”, „dezaktywuj tego użytkownika”, „którzy użytkownicy są obecnie aktywowani?” i „jaka jest nazwa tego użytkownika?”. W klasycznym ujęciu: chodzi o zapytania i polecenia .

Myślenie w poleceniach

Zacznijmy od spojrzenia na polecenia w twoim przykładzie: „aktywuj tego użytkownika” i „dezaktywuj tego użytkownika”. Zaletą poleceń jest to, że można je łatwo wyrazić za pomocą małych scenariuszy:

podane nieaktywny użytkownik
, gdy administrator aktywuje ten użytkownik
następnie użytkownik staje się aktywny
i potwierdzenie e-mail zostanie wysłana do użytkownika
, a wpis zostanie dodany do dziennika systemowego
(itd. itd.)

Takie scenariusze są przydatne, aby zobaczyć, jak jedno polecenie może wpłynąć na różne części infrastruktury - w tym przypadku baza danych (jakiś rodzaj „aktywnej” flagi), serwer poczty, dziennik systemowy itp.

Taki scenariusz naprawdę pomaga w skonfigurowaniu środowiska programistycznego opartego na testach.

I wreszcie, myślenie w poleceniach naprawdę pomaga stworzyć aplikację zorientowaną na zadania. Twoi użytkownicy docenią to :-)

Wyrażanie poleceń

Django zapewnia dwa proste sposoby wyrażania poleceń; oba są poprawnymi opcjami i nie jest niczym niezwykłym połączenie obu podejść.

Warstwa usługi

Moduł serwisowy został już opisany przez @Hedde . Tutaj definiujesz oddzielny moduł, a każde polecenie jest reprezentowane jako funkcja.

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

Korzystanie z formularzy

Innym sposobem jest użycie formularza Django dla każdego polecenia. Wolę to podejście, ponieważ łączy w sobie wiele ściśle powiązanych aspektów:

  • wykonanie polecenia (co on robi?)
  • sprawdzanie poprawności parametrów polecenia (czy można to zrobić?)
  • prezentacja polecenia (jak mogę to zrobić?)

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

Myślenie w zapytaniach

Twój przykład nie zawierał żadnych zapytań, więc skorzystałem z kilku przydatnych zapytań. Wolę używać terminu „pytanie”, ale zapytania to klasyczna terminologia. Interesujące zapytania to: „Jak nazywa się ten użytkownik?”, „Czy ten użytkownik może się zalogować?”, „Pokaż listę dezaktywowanych użytkowników” i „Jaki jest rozkład geograficzny dezaktywowanych użytkowników?”

Zanim zaczniesz odpowiadać na te zapytania, powinieneś zawsze zadać sobie dwa pytania: czy jest to zapytanie prezentacyjne tylko dla moich szablonów i / lub zapytanie logiki biznesowej związane z wykonywaniem moich poleceń i / lub zapytanie sprawozdawcze .

Zapytania prezentacyjne mają na celu jedynie ulepszenie interfejsu użytkownika. Odpowiedzi na zapytania dotyczące logiki biznesowej bezpośrednio wpływają na wykonywanie poleceń. Zapytania sprawozdawcze służą wyłącznie celom analitycznym i mają luźniejsze ograniczenia czasowe. Te kategorie nie wykluczają się wzajemnie.

Drugie pytanie brzmi: „czy mam pełną kontrolę nad odpowiedziami?” Na przykład podczas zapytania o nazwę użytkownika (w tym kontekście) nie mamy żadnej kontroli nad wynikiem, ponieważ polegamy na zewnętrznym interfejsie API.

Robienie zapytań

Najbardziej podstawowym zapytaniem w Django jest użycie obiektu Manager:

User.objects.filter(active=True)

Oczywiście działa to tylko wtedy, gdy dane są faktycznie reprezentowane w modelu danych. Nie zawsze tak jest. W takich przypadkach możesz rozważyć poniższe opcje.

Niestandardowe tagi i filtry

Pierwsza alternatywa jest przydatna w przypadku zapytań, które mają jedynie charakter prezentacyjny: tagi niestandardowe i filtry szablonów.

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)

Metody zapytań

Jeśli twoje zapytanie nie jest wyłącznie prezentacyjne, możesz dodać zapytania do services.py (jeśli go używasz) lub wprowadzić moduł 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 

Modele proxy

Modele proxy są bardzo przydatne w kontekście logiki biznesowej i raportowania. Zasadniczo definiujesz ulepszony podzbiór swojego modelu. Można zastąpić podstawowy QuerySet menedżera, zastępując Manager.get_queryset()metodę.

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

Modele zapytań

W przypadku zapytań, które są z natury złożone, ale są wykonywane dość często, istnieje możliwość modeli zapytań. Model zapytania jest formą denormalizacji, w której odpowiednie dane dla pojedynczego zapytania są przechowywane w osobnym modelu. Sztuczka polega oczywiście na utrzymaniu zsynchronizowanego modelu w synchronizacji z modelem podstawowym. Z modeli zapytań można korzystać tylko wtedy, gdy zmiany są całkowicie pod twoją kontrolą.

models.py

class InactiveUserDistribution(models.Model):
    country = CharField(max_length=200)
    inactive_user_count = IntegerField(default=0)

Pierwszą opcją jest aktualizacja tych modeli w twoich poleceniach. Jest to bardzo przydatne, jeśli modele te są zmieniane tylko za pomocą jednego lub dwóch poleceń.

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

Lepszą opcją byłoby użycie niestandardowych sygnałów. Sygnały te są oczywiście emitowane przez twoje polecenia. Zaletą sygnałów jest to, że można zsynchronizować wiele modeli zapytań z modelem oryginalnym. Ponadto przetwarzanie sygnału można odciążyć do zadań w tle, używając Selera lub podobnych ram.

sygnały.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()

Utrzymanie w czystości

Stosując to podejście, niezwykle łatwo jest ustalić, czy kod pozostaje czysty. Postępuj zgodnie z tymi wskazówkami:

  • Czy mój model zawiera metody, które robią więcej niż zarządzanie stanem bazy danych? Powinieneś wyodrębnić polecenie.
  • Czy mój model zawiera właściwości, które nie są mapowane na pola bazy danych? Powinieneś wyodrębnić zapytanie.
  • Czy mój model odwołuje się do infrastruktury, która nie jest moją bazą danych (np. Poczta)? Powinieneś wyodrębnić polecenie.

To samo dotyczy wyświetleń (ponieważ widoki często mają ten sam problem).

  • Czy mój widok aktywnie zarządza modelami baz danych? Powinieneś wyodrębnić polecenie.

Niektóre referencje

Dokumentacja Django: modele proxy

Dokumentacja Django: sygnały

Architektura: Projektowanie oparte na domenie

publysher
źródło
11
Miło jest widzieć odpowiedź zawierającą DDD w pytaniu związanym z django. Tylko dlatego, że Django stosuje ActiveRecord do uporczywości, nie oznacza, że ​​rozdzielenie problemów powinno wyjść za okno. Świetna odpowiedź.
Scott Coates,
6
Jeśli chcę sprawdzić, czy użytkownik, który został zablokowany, jest właścicielem obiektu przed usunięciem tego obiektu, czy powinienem to sprawdzić w widoku lub w formularzu / module usługi?
Ivan
6
@Ivan: oba. To musi być w module forma / usługi, ponieważ jest ona częścią swoich ograniczeń biznesowych. To powinno być również w widoku ponieważ należy tylko obecne działania, które użytkownicy mogą faktycznie wykonać.
publysher
4
Kierownik zwyczaj metody są dobrym sposobem realizacji zapytania: User.objects.inactive_users(). Ale przykładowy model proxy tutaj IMO prowadzi do niepoprawnej semantyki: u = InactiveUser.objects.all()[0]; u.active = True; u.save()a jednak isinstance(u, InactiveUser) == True. Wspomniałbym również, że skutecznym sposobem na utrzymanie modelu zapytań w wielu przypadkach jest widok db.
Aryeh Leib Taurog
1
@adnanmuttaleb To prawda. Pamiętaj, że sama odpowiedź używa tylko terminu „model domeny”. Zamieściłem link do DDD nie dlatego, że moja odpowiedź brzmi DDD, ale dlatego, że ta książka świetnie pomaga ci myśleć o modelach domen.
publysher
148

Zwykle implementuję warstwę usług pomiędzy widokami i modelami. To działa jak interfejs API twojego projektu i daje ci dobry widok helikoptera na to, co się dzieje. Odziedziczyłem tę praktykę od mojego kolegi, który bardzo często stosuje tę technikę warstw w projektach Java (JSF), np .:

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

Pamiętaj, że zwykle przenoszę modele, widoki i usługi na poziom modułu i oddzielam je jeszcze bardziej, w zależności od wielkości projektu

Hedde van der Heide
źródło
8
Podoba mi się podejście ogólne, ale z mojego zrozumienia Twój konkretny przykład byłby zwykle wdrażany jako menedżer .
arie
9
@arie niekoniecznie, być może lepszy przykład, dla usług sklepu internetowego może obejmować generowanie sesji koszyka, asynchroniczne zadania, takie jak obliczanie ocen produktów, tworzenie i wysyłanie e-maili itp.
Hedde van der Heide
4
Podobało mi się również to podejście, również z java. Jestem nowym użytkownikiem Pythona. Jak przetestowałbyś views.py? Jak wyśmiewałbyś warstwę usługi (jeśli na przykład usługa wykonuje niektóre zdalne połączenia API)?
Teimuraz,
71

Przede wszystkim nie powtarzaj się .

Następnie uważaj, aby nie prześcignąć inżyniera, czasem jest to strata czasu i sprawia, że ​​ktoś traci koncentrację na tym, co ważne. Przeglądu Zen pytona od czasu do czasu.

Zobacz aktywne projekty

  • więcej osób = więcej trzeba odpowiednio zorganizować
  • że Django przechowalni mają prostą strukturę.
  • repozytorium pip mają strukturę katalogów straigtforward.
  • repozytorium tkanina jest również jeden dobry patrzeć.

    • możesz umieścić wszystkie swoje modele yourapp/models/logicalgroup.py
  • np User, Groupi związane z nimi modele mogą przejść podyourapp/models/users.py
  • np Poll, Question,Answer ... może pójść podyourapp/models/polls.py
  • załaduj to, czego potrzebujesz w __all__środkuyourapp/models/__init__.py

Więcej o MVC

  • model to twoje dane
    • dotyczy to twoich rzeczywistych danych
    • dotyczy to również danych sesji / pliku cookie / pamięci podręcznej / fs / indeksu
  • użytkownik wchodzi w interakcję z kontrolerem, aby manipulować modelem
    • może to być interfejs API lub widok, który zapisuje / aktualizuje dane
    • można to dostroić za pomocą request.GET/request.POST ... itd
    • pomyśl też o stronicowaniu lub filtrowaniu .
  • dane aktualizują widok
    • szablony pobierają dane i odpowiednio je formatują
    • Interfejsy API nawet bez szablonów są częścią widoku; na przykładtastypie lubpiston
    • powinno to również uwzględniać oprogramowanie pośrednie.

Skorzystaj z oprogramowania pośredniego / szablonów

  • Jeśli potrzebujesz trochę pracy do wykonania przy każdym żądaniu, oprogramowanie pośrednie jest jednym ze sposobów.
    • np. dodawanie znaczników czasu
    • np. aktualizowanie wskaźników dotyczących odwiedzin strony
    • np. zapełnianie pamięci podręcznej
  • Jeśli masz fragmenty kodu, które zawsze powtarzają się w celu formatowania obiektów, szablony są dobre.
    • np. aktywna bułka tarta / url

Skorzystaj z menedżerów modeli

  • tworzenie Usermoże przejść w UserManager(models.Manager).
  • Krwawe szczegóły dla instancji powinny przejść do models.Model.
  • Krwawe szczegóły dla querysetmogą iść w models.Manager.
  • możesz chcieć utworzyć Userjeden na raz, więc możesz pomyśleć, że powinien on żyć na samym modelu, ale podczas tworzenia obiektu prawdopodobnie nie masz wszystkich szczegółów:

Przykład:

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

W miarę możliwości korzystaj z formularzy

Wiele kodów typu „płyta podstawowa” można wyeliminować, jeśli masz formularze odwzorowujące model. To ModelForm documentationjest całkiem dobre. Oddzielanie kodu formularzy od kodu modelu może być dobre, jeśli masz wiele możliwości dostosowywania (lub czasami unikniesz cyklicznych błędów importu w przypadku bardziej zaawansowanych zastosowań).

Jeśli to możliwe, używaj poleceń zarządzania

  • na przykład yourapp/management/commands/createsuperuser.py
  • na przykład yourapp/management/commands/activateinbulk.py

jeśli masz logikę biznesową, możesz ją rozdzielić

  • django.contrib.auth używa backendów , podobnie jak db ma backend ... itd.
  • dodaj settinglogikę biznesową (np. AUTHENTICATION_BACKENDS)
  • możesz użyć django.contrib.auth.backends.RemoteUserBackend
  • możesz użyć yourapp.backends.remote_api.RemoteUserBackend
  • możesz użyć yourapp.backends.memcached.RemoteUserBackend
  • przekazać trudną logikę biznesową do backendu
  • upewnij się, że ustawiłeś oczekiwanie bezpośrednio na wejściu / wyjściu.
  • zmiana logiki biznesowej jest tak prosta jak zmiana ustawienia :)

przykład backendu:

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' 

może stać się:

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

więcej o wzorach projektowych

więcej o granicach interfejsu

  • Czy kod, którego chcesz użyć, naprawdę jest częścią modeli? ->yourapp.models
  • Czy kod jest częścią logiki biznesowej? ->yourapp.vendor
  • Czy kod jest częścią ogólnych narzędzi / bibliotek? ->yourapp.libs
  • Czy kod jest częścią bibliotek logiki biznesowej? -> yourapp.libs.vendorlubyourapp.vendor.libs
  • Oto dobry przykład: czy możesz samodzielnie przetestować kod?
    • tak dobrze :)
    • nie, możesz mieć problem z interfejsem
    • gdy istnieje wyraźny rozdział powinien unittest być proste z wykorzystaniem kpin
  • Czy separacja jest logiczna?
    • tak dobrze :)
    • nie, możesz mieć problemy z osobnym przetestowaniem tych logicznych koncepcji.
  • Czy uważasz, że będziesz musiał dokonać refaktoryzacji, gdy otrzymasz 10 razy więcej kodu?
    • tak, nie dobrze, nie ma bueno, refaktor może być bardzo pracochłonny
    • nie, to po prostu niesamowite!

Krótko mówiąc, mógłbyś

  • 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

lub cokolwiek innego, co ci pomoże; znalezienie potrzebnych interfejsów i granic pomoże.

dnozay
źródło
27

Django stosuje nieco zmodyfikowany rodzaj MVC. W Django nie ma koncepcji „kontrolera”. Najbliższym serwerem proxy jest „widok”, który powoduje zamieszanie przy konwersji MVC, ponieważ w MVC widok bardziej przypomina „szablon” Django.

W Django „model” nie jest jedynie abstrakcją bazy danych. Pod pewnymi względami dzieli on obowiązek z „poglądem” Django na kontrolera MVC. Zawiera całość zachowań związanych z instancją. Jeśli to wystąpienie musi wchodzić w interakcję z zewnętrznym interfejsem API w ramach jego zachowania, to nadal jest to kod modelu. W rzeczywistości modele nie są w ogóle wymagane do interakcji z bazą danych, więc można sobie wyobrazić, że modele istnieją całkowicie jako warstwa interaktywna dla zewnętrznego interfejsu API. Jest to o wiele bardziej darmowa koncepcja „modelu”.

Chris Pratt
źródło
7

W Django struktura MVC jest, jak powiedział Chris Pratt, odmienna od klasycznego modelu MVC stosowanego w innych ramach, myślę, że głównym powodem tego jest unikanie zbyt ścisłej struktury aplikacji, jak ma to miejsce w innych ramach MVC, takich jak CakePHP.

W Django MVC zostało zaimplementowane w następujący sposób:

Warstwa widoku jest podzielona na dwie części. Widoki powinny być używane tylko do zarządzania żądaniami HTTP, są one wywoływane i odpowiadają na nie. Widoki komunikują się z resztą aplikacji (formularze, formularze modeli, klasy niestandardowe, w prostych przypadkach bezpośrednio z modelami). Do stworzenia interfejsu używamy szablonów. Szablony są podobne do napisów w Django, odwzorowuje w nich kontekst, a ten kontekst został przekazany do widoku przez aplikację (kiedy widok o to poprosi).

Warstwa modelu zapewnia enkapsulację, abstrakcję, sprawdzanie poprawności, inteligencję i sprawia, że ​​twoje dane są zorientowane obiektowo (mówią, że pewnego dnia DBMS również to zrobi). Nie oznacza to, że powinieneś tworzyć ogromne pliki models.py (w rzeczywistości bardzo dobrą radą jest podzielenie modeli na różne pliki, umieszczenie ich w folderze zwanym „modelami”, utworzenie w tym pliku pliku „__init__.py” folder, w którym zaimportujesz wszystkie swoje modele i na koniec użyjesz atrybutu „app_label” modeli.Model). Model powinien oderwać Cię od pracy z danymi, dzięki czemu Twoja aplikacja będzie prostsza. W razie potrzeby powinieneś również utworzyć klasy zewnętrzne, takie jak „narzędzia” dla swoich modeli. Możesz także używać dziedzictwa w modelach, ustawiając atrybut „abstrakcyjny” klasy Meta swojego modelu na „True”.

Gdzie jest reszta? Cóż, małe aplikacje internetowe są na ogół swego rodzaju interfejsem do danych, w niektórych małych przypadkach programowych wystarczyłoby użyć widoków do zapytania lub wstawienia danych. Bardziej powszechne przypadki wykorzystują Formularze lub ModelFormy, które w rzeczywistości są „kontrolerami”. Nie jest to nic innego jak praktyczne rozwiązanie typowego problemu i bardzo szybki. Tego właśnie używa strona internetowa.

Jeśli formularze nie są dla ciebie enogh, powinieneś stworzyć własne klasy, aby wykonać magię, bardzo dobrym przykładem jest aplikacja administracyjna: możesz odczytać kod ModelAmin, to faktycznie działa jako kontroler. Nie ma standardowej struktury, proponuję zbadać istniejące aplikacje Django, zależy to od każdego przypadku. Tak zamierzali programiści Django: możesz dodać klasę parsera XML, klasę interfejsu API, dodać Selera do wykonywania zadań, przekręcać dla aplikacji opartej na reaktorach, używać tylko ORM, tworzyć serwis internetowy, modyfikować aplikację administracyjną i więcej. .. Twoim obowiązkiem jest tworzenie dobrej jakości kodu, szanowanie filozofii MVC lub nie, tworzenie bazujących na modułach i tworzenie własnych warstw abstrakcji. Jest bardzo elastyczny.

Moja rada: przeczytaj jak najwięcej kodu, istnieje wiele aplikacji django, ale nie bierz ich tak poważnie. Każdy przypadek jest inny, wzory i teoria pomagają, ale nie zawsze, jest to nieprecyzyjny przypadek, django zapewnia tylko dobre narzędzia, których możesz użyć, aby ożywić niektóre problemy (takie jak interfejs administratora, sprawdzanie poprawności formularza internetowego, i18n, implementacja wzorca obserwatora, wszystko wspomniane wcześniej i inne), ale dobre projekty pochodzą od doświadczonych projektantów.

PS .: użyj klasy „User” z aplikacji auth (ze standardowego django), możesz na przykład utworzyć profile użytkowników lub przynajmniej odczytać jego kod, będzie to przydatne w twoim przypadku.

Nate Gentile
źródło
1

Stare pytanie, ale i tak chciałbym zaoferować swoje rozwiązanie. Opiera się na akceptacji, że obiekty modelu również wymagają dodatkowej funkcjonalności, podczas gdy niewygodne jest umieszczanie ich w pliku models.py . Ciężka logika biznesowa może być napisana osobno w zależności od osobistych upodobań, ale przynajmniej podoba mi się ten model, aby robić wszystko, co związane z samym sobą. To rozwiązanie obsługuje również tych, którzy lubią mieć całą logikę w samych modelach.

Jako taki, wymyśliłem hack, który pozwala mi oddzielić logikę od definicji modelu i nadal uzyskiwać wszystkie wskazówki z mojego IDE.

Korzyści powinny być oczywiste, ale zawiera kilka zaobserwowanych przeze mnie:

  • Definicje DB pozostają takie same - nie dołączono logicznego „śmieci”
  • Logika związana z modelem jest umieszczona starannie w jednym miejscu
  • Wszystkie usługi (formularze, REST, widoki) mają jeden punkt dostępu do logiki
  • A co najlepsze: nie musiałem przepisywać żadnego kodu, kiedy zdałem sobie sprawę, że mój models.py stał się zbyt zagracony i musiałem oddzielić logikę. Rozdzielenie jest płynne i iteracyjne: Mógłbym wykonać funkcję w czasie lub całej klasie lub całego modelus.py.

Korzystałem z tego w Pythonie 3.4 i nowszych oraz Django 1.8 i nowszych.

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

Jedyne, czego nie mogę zrozumieć, to jak sprawić, aby moje IDE (w tym przypadku PyCharm) rozpoznało, że UserLogic jest w rzeczywistości modelem użytkownika. Ale ponieważ jest to oczywiście włamanie, z przyjemnością akceptuję małe niedogodności związane z zawsze określaniem typu selfparametru.

velis
źródło
Właściwie uważam to za łatwe w użyciu podejście. Ale chciałbym przenieść ostateczny model do innego pliku i nie dziedziczyć w models.py. To byłoby jak service.py, gdyby starła się logika użytkownika + model
Maks
1

Musiałbym się z tobą zgodzić. Istnieje wiele możliwości w Django, ale najlepiej zacząć od przeglądu filozofii projektowania Django .

  1. Wywołanie interfejsu API z właściwości modelu nie byłoby idealne, wydaje się, że bardziej sensowne byłoby zrobienie czegoś takiego w widoku i ewentualnie stworzenie warstwy usług, aby utrzymać suchość. Jeśli wywołanie interfejsu API nie jest blokowane, a wywołanie jest kosztowne, sensowne może być wysłanie żądania do pracownika usługi (pracownika, który korzysta z kolejki).

  2. Zgodnie z filozofią projektowania Django modele obejmują każdy aspekt „obiektu”. Dlatego cała logika biznesowa związana z tym obiektem powinna tam istnieć:

Uwzględnij całą odpowiednią logikę domeny

Modele powinny obejmować każdy aspekt „obiektu”, zgodnie z wzorcem projektowym Active Record Martina Fowlera.

  1. Opisane przez ciebie efekty uboczne są widoczne, logikę tutaj można lepiej podzielić na zestawy zapytań i menedżerów. Oto przykład:

    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)
radtek
źródło
0

W większości zgadzam się z wybraną odpowiedzią ( https://stackoverflow.com/a/12857584/871392 ), ale chcę dodać opcję w sekcji Tworzenie zapytań.

Można zdefiniować klasy QuerySet dla modeli do tworzenia zapytań filtrów i tak dalej. Następnie możesz proxy tej klasy zestawów zapytań dla menedżera modelu, podobnie jak wbudowane klasy Menedżera i klasy QuerySet.

Chociaż, jeśli trzeba zapytać o kilka modeli danych, aby uzyskać jeden model domeny, wydaje mi się rozsądniejsze, aby umieścić to w osobnym module, jak sugerowano wcześniej.

l0ki
źródło
0

Najbardziej obszerny artykuł na temat różnych opcji z zaletami i wadami:

  1. Pomysł nr 1: Modele tłuszczu
  2. Pomysł nr 2: Umieszczenie logiki biznesowej w widokach / formularzach
  3. Pomysł nr 3: Usługi
  4. Pomysł nr 4: QuerySets / Managers
  5. Wniosek

Źródło: https://sunscrapers.com/blog/where-to-put-business-logic-django/

FSE
źródło
Powinieneś dodać jakieś wyjaśnienie.
m02ph3u5
-6

Django został zaprojektowany w taki sposób, aby można go było łatwo wykorzystywać do dostarczania stron internetowych. Jeśli nie czujesz się z tym komfortowo, być może powinieneś użyć innego rozwiązania.

Piszę root lub typowe operacje na modelu (aby mieć ten sam interfejs) i inne na kontrolerze modelu. Jeśli potrzebuję operacji z innego modelu, importuję jej kontroler.

To podejście wystarcza mi i złożoności moich aplikacji.

Odpowiedź Hedde jest przykładem pokazującym elastyczność samego django i samego pythona.

W każdym razie bardzo interesujące pytanie!

pvilas
źródło
9
Jak powiedzenie, że jest wystarczająco dobre, by pomóc mi zrozumieć twoje pytanie?
Chris Wesseling,
1
Django ma znacznie więcej do zaoferowania niż modele django.db.m, ale większość ekosystemu zależy w dużej mierze od modelu używającego modeli django.
andho
1
Wzorzec projektowy wykorzystywany do tworzenia oprogramowania. I django zaprojektowany do łatwego użycia do dostarczania oprogramowania w średniej lub dużej skali, nie tylko stron internetowych!
Mohammad Torkashvand