Trennung von Geschäftslogik und Datenzugriff in Django

484

Ich schreibe ein Projekt in Django und sehe, dass 80% des Codes in der Datei enthalten sind models.py. Dieser Code ist verwirrend und nach einer gewissen Zeit verstehe ich nicht mehr, was wirklich passiert.

Folgendes stört mich:

  1. Ich finde es hässlich, dass meine Modellebene (die eigentlich nur für die Arbeit mit Daten aus einer Datenbank verantwortlich sein sollte) auch E-Mails sendet, über API an andere Dienste geht usw.
  2. Außerdem finde ich es nicht akzeptabel, Geschäftslogik in die Ansicht aufzunehmen, da es auf diese Weise schwierig wird, sie zu kontrollieren. In meiner Anwendung gibt es beispielsweise mindestens drei Möglichkeiten, neue Instanzen von zu erstellen User, aber technisch sollte sie einheitlich erstellt werden.
  3. Ich merke nicht immer, wann die Methoden und Eigenschaften meiner Modelle nicht deterministisch werden und wann sie Nebenwirkungen entwickeln.

Hier ist ein einfaches Beispiel. Das UserModell war zunächst so:

class User(db.Models):

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

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

Im Laufe der Zeit wurde daraus:

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

Ich möchte Entitäten in meinem Code trennen:

  1. Entitäten meiner Datenbank, Datenbankebene: Was enthält meine Anwendung?
  2. Entitäten meiner Anwendung, Geschäftslogikebene: Was kann meine Anwendung ausmachen?

Was sind die guten Praktiken, um einen solchen Ansatz umzusetzen, der in Django angewendet werden kann?

defuz
quelle
14
Lesen Sie über Signale
Konstant
1
Nun, Sie haben das Tag entfernt, aber Sie könnten DCI verwenden, um die Trennung von dem, was das System tut (die Funktionalität) und was das System ist (das Daten- / Domänenmodell), zu erreichen
Rune FS
2
Sie schlagen vor, die gesamte Geschäftslogik in Signalrückrufen zu implementieren? Leider können nicht alle meiner Anwendungen mit Ereignissen in der Datenbank verknüpft werden.
defuz
Rune FS, ich habe versucht, das DCI zu verwenden, aber es schien mir, dass es für mein Projekt nicht viel braucht: Kontext, Definition von Rollen als Mixin zu den Objekten usw. Es gibt eine einfachere Art der Trennung von "tut" und "tut". ist "? Könnten Sie ein minimales Beispiel geben?
defuz

Antworten:

635

Anscheinend fragen Sie nach dem Unterschied zwischen dem Datenmodell und dem Domänenmodell. Bei letzterem finden Sie die Geschäftslogik und Entitäten, die von Ihrem Endbenutzer wahrgenommen werden. Bei ersteren speichern Sie Ihre Daten tatsächlich.

Darüber hinaus habe ich den dritten Teil Ihrer Frage wie folgt interpretiert: Wie kann man feststellen, dass diese Modelle nicht getrennt bleiben?

Dies sind zwei sehr unterschiedliche Konzepte, und es ist immer schwierig, sie getrennt zu halten. Es gibt jedoch einige gängige Muster und Werkzeuge, die für diesen Zweck verwendet werden können.

Informationen zum Domänenmodell

Das erste, was Sie erkennen müssen, ist, dass es in Ihrem Domain-Modell nicht wirklich um Daten geht. Es geht um Aktionen und Fragen wie "Diesen Benutzer aktivieren", "Diesen Benutzer deaktivieren", "Welche Benutzer sind derzeit aktiviert?" und "Wie heißt dieser Benutzer?". Klassisch ausgedrückt: Es geht um Abfragen und Befehle .

In Befehlen denken

Schauen wir uns zunächst die Befehle in Ihrem Beispiel an: "Diesen Benutzer aktivieren" und "Diesen Benutzer deaktivieren". Das Schöne an Befehlen ist, dass sie leicht durch kleine gegebene Wann-Dann-Szenarien ausgedrückt werden können:

gegeben ein inaktiver Benutzer ,
wenn der Administrator dieser Benutzer aktiviert
dann der Benutzer aktiv wird ,
und eine Bestätigungs - E-Mail an den Benutzer gesendet wird ,
und ein Eintrag wird in das Systemprotokoll hinzugefügt
(etc. etc.)

Solche Szenarien sind nützlich, um zu sehen, wie verschiedene Teile Ihrer Infrastruktur von einem einzigen Befehl beeinflusst werden können - in diesem Fall Ihre Datenbank (eine Art 'aktives' Flag), Ihr Mailserver, Ihr Systemprotokoll usw.

Solche Szenarien helfen Ihnen auch beim Einrichten einer testgesteuerten Entwicklungsumgebung.

Und schließlich hilft Ihnen das Denken in Befehlen wirklich dabei, eine aufgabenorientierte Anwendung zu erstellen. Ihre Benutzer werden dies zu schätzen wissen :-)

Befehle ausdrücken

Django bietet zwei einfache Möglichkeiten, Befehle auszudrücken. Sie sind beide gültige Optionen und es ist nicht ungewöhnlich, die beiden Ansätze zu mischen.

Die Serviceschicht

Das Servicemodul wurde bereits von @Hedde beschrieben . Hier definieren Sie ein separates Modul und jeder Befehl wird als Funktion dargestellt.

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

Formulare verwenden

Die andere Möglichkeit besteht darin, für jeden Befehl ein Django-Formular zu verwenden. Ich bevorzuge diesen Ansatz, weil er mehrere eng verwandte Aspekte kombiniert:

  • Ausführung des Befehls (was macht er?)
  • Validierung der Befehlsparameter (kann das?)
  • Präsentation des Befehls (wie kann ich das machen?)

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

In Fragen denken

Ihr Beispiel enthielt keine Abfragen, daher habe ich mir erlaubt, einige nützliche Abfragen zu erstellen. Ich bevorzuge den Begriff "Frage", aber Abfragen sind die klassische Terminologie. Interessante Fragen sind: "Wie heißt dieser Benutzer?", "Kann sich dieser Benutzer anmelden?", "Liste der deaktivierten Benutzer anzeigen" und "Wie ist die geografische Verteilung der deaktivierten Benutzer?"

Bevor Sie mit der Beantwortung dieser Fragen beginnen, sollten Sie sich immer zwei Fragen stellen: Ist dies eine Präsentationsabfrage nur für meine Vorlagen und / oder eine Geschäftslogikabfrage, die an die Ausführung meiner Befehle gebunden ist, und / oder eine Berichtsabfrage .

Präsentationsabfragen dienen lediglich der Verbesserung der Benutzeroberfläche. Die Antworten auf Geschäftslogikabfragen wirken sich direkt auf die Ausführung Ihrer Befehle aus. Berichtsabfragen dienen lediglich analytischen Zwecken und unterliegen weniger zeitlichen Einschränkungen. Diese Kategorien schließen sich nicht gegenseitig aus.

Die andere Frage lautet: "Habe ich die vollständige Kontrolle über die Antworten?" Wenn wir beispielsweise den Benutzernamen abfragen (in diesem Zusammenhang), haben wir keine Kontrolle über das Ergebnis, da wir auf eine externe API angewiesen sind.

Abfragen stellen

Die grundlegendste Abfrage in Django ist die Verwendung des Manager-Objekts:

User.objects.filter(active=True)

Dies funktioniert natürlich nur, wenn die Daten tatsächlich in Ihrem Datenmodell dargestellt werden. Dies ist nicht immer der Fall. In diesen Fällen können Sie die folgenden Optionen in Betracht ziehen.

Benutzerdefinierte Tags und Filter

Die erste Alternative ist nützlich für Abfragen, die nur zur Präsentation dienen: benutzerdefinierte Tags und Vorlagenfilter.

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)

Abfragemethoden

Wenn Ihre Abfrage nicht nur präsentativ ist, können Sie Ihrer services.py Abfragen hinzufügen (falls Sie diese verwenden) oder ein queries.py- Modul einführen :

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 

Proxy-Modelle

Proxy-Modelle sind im Kontext von Geschäftslogik und Berichterstellung sehr nützlich. Sie definieren grundsätzlich eine erweiterte Teilmenge Ihres Modells. Sie können das Basis-QuerySet eines Managers überschreiben, indem Sie die Manager.get_queryset()Methode überschreiben .

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

Abfragemodelle

Für Abfragen, die von Natur aus komplex sind, aber häufig ausgeführt werden, besteht die Möglichkeit von Abfragemodellen. Ein Abfragemodell ist eine Form der Denormalisierung, bei der relevante Daten für eine einzelne Abfrage in einem separaten Modell gespeichert werden. Der Trick besteht natürlich darin, das denormalisierte Modell mit dem primären Modell synchron zu halten. Abfragemodelle können nur verwendet werden, wenn Änderungen vollständig unter Ihrer Kontrolle stehen.

models.py

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

Die erste Möglichkeit besteht darin, diese Modelle in Ihren Befehlen zu aktualisieren. Dies ist sehr nützlich, wenn diese Modelle nur durch einen oder zwei Befehle geändert werden.

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

Eine bessere Option wäre die Verwendung von benutzerdefinierten Signalen. Diese Signale werden natürlich von Ihren Befehlen ausgegeben. Signale haben den Vorteil, dass Sie mehrere Abfragemodelle mit Ihrem ursprünglichen Modell synchronisieren können. Darüber hinaus kann die Signalverarbeitung mithilfe von Sellerie oder ähnlichen Frameworks auf Hintergrundaufgaben verlagert werden.

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

Halten Sie es sauber

Wenn Sie diesen Ansatz verwenden, wird es lächerlich einfach festzustellen, ob Ihr Code sauber bleibt. Befolgen Sie einfach diese Richtlinien:

  • Enthält mein Modell Methoden, die mehr als nur den Datenbankstatus verwalten? Sie sollten einen Befehl extrahieren.
  • Enthält mein Modell Eigenschaften, die nicht Datenbankfeldern zugeordnet sind? Sie sollten eine Abfrage extrahieren.
  • Verweist mein Modell auf eine Infrastruktur, die nicht meine Datenbank ist (z. B. E-Mail)? Sie sollten einen Befehl extrahieren.

Gleiches gilt für Ansichten (da Ansichten häufig unter demselben Problem leiden).

  • Verwaltet meine Ansicht aktiv Datenbankmodelle? Sie sollten einen Befehl extrahieren.

Einige Referenzen

Django-Dokumentation: Proxy-Modelle

Django-Dokumentation: Signale

Architektur: Domain Driven Design

Publysher
quelle
11
Es ist schön, eine Antwort zu sehen, die DDD in eine Django-bezogene Frage einbezieht. Nur weil Django ActiveRecord für die Persistenz einsetzt, bedeutet dies nicht, dass die Trennung von Bedenken aus dem Fenster gehen sollte. Gute Antwort.
Scott Coates
6
Wenn ich überprüfen möchte, ob der geloggte Benutzer der Eigentümer eines Objekts ist, bevor ich dieses Objekt lösche, sollte ich dies in der Ansicht oder im Formular- / Servicemodul überprüfen?
Ivan
6
@ Ivan: beides. Es muss sich im Formular- / Servicemodul befinden, da es Teil Ihrer geschäftlichen Einschränkungen ist. Es sollte auch in der Ansicht sein, da Sie nur Aktionen präsentieren sollten, die Benutzer tatsächlich ausführen können.
Publysher
4
Benutzerdefinierte Manager- Methoden sind eine gute Möglichkeit, Abfragen zu implementieren : User.objects.inactive_users(). Aber das Proxy-Modell-Beispiel hier IMO führt zu einer falschen Semantik: u = InactiveUser.objects.all()[0]; u.active = True; u.save()und doch isinstance(u, InactiveUser) == True. Ich würde auch erwähnen, dass ein effektiver Weg, ein Abfragemodell in vielen Fällen zu pflegen, eine Datenbankansicht ist.
Aryeh Leib Taurog
1
@adnanmuttaleb Das ist richtig. Beachten Sie, dass in der Antwort selbst nur der Begriff "Domänenmodell" verwendet wird. Ich habe den Link zu DDD nicht eingefügt, weil meine Antwort DDD lautet, sondern weil dieses Buch Ihnen dabei hilft, über Domain-Modelle nachzudenken.
Publysher
148

Normalerweise implementiere ich eine Service-Schicht zwischen Ansichten und Modellen. Dies verhält sich wie die API Ihres Projekts und gibt Ihnen einen guten Überblick über die Vorgänge im Hubschrauber. Ich habe diese Praxis von einem Kollegen geerbt, der diese Überlagerungstechnik häufig bei Java-Projekten (JSF) verwendet, z.

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

Wohlgemerkt, ich bringe Modelle, Ansichten und Dienste normalerweise auf Modulebene und trenne sie je nach Projektgröße noch weiter

Hedde van der Heide
quelle
8
Ich mag den allgemeinen Ansatz, obwohl nach meinem Verständnis Ihr spezifisches Beispiel normalerweise als Manager implementiert wird .
Arie
9
@arie nicht unbedingt, vielleicht ein besseres Beispiel, für einen Webshop-Service wäre Dinge wie das Generieren von Warenkorbsitzungen, asynchrone Aufgaben wie Produktbewertungsberechnungen, Erstellen und Senden von E-Mails usw.
Hedde van der Heide
4
Ich mag diesen Ansatz auch und komme auch aus Java. Ich bin neu in Python. Wie würden Sie views.py testen? Wie würden Sie die Service-Schicht verspotten (wenn der Service beispielsweise einige Remote-API-Aufrufe ausführt)?
Teimuraz
71

Zunächst einmal, Sie wiederholen sich nicht .

Dann achten Sie bitte darauf, nicht zu überarbeiten, manchmal ist es nur Zeitverschwendung und lässt jemanden den Fokus auf das verlieren, was wichtig ist. Überprüfen Sie von Zeit zu Zeit den Zen von Python .

Schauen Sie sich aktive Projekte an

  • mehr Menschen = mehr müssen sich richtig organisieren
  • Im Django-Repository haben sie eine einfache Struktur.
  • Im Pip-Repository haben sie eine geradlinige Verzeichnisstruktur.
  • Das Fabric-Repository ist auch gut anzusehen.

    • Sie können alle Ihre Modelle unter platzieren yourapp/models/logicalgroup.py
  • zB User, Groupund verwandte Modelle gehen unteryourapp/models/users.py
  • zB Poll, Question, Answer... könnte gehen unteryourapp/models/polls.py
  • laden , was Sie benötigen __all__innerhalb vonyourapp/models/__init__.py

Mehr über MVC

  • Modell ist Ihre Daten
    • Dies schließt Ihre tatsächlichen Daten ein
    • Dies schließt auch Ihre Sitzungs- / Cookie- / Cache- / Fs- / Indexdaten ein
  • Der Benutzer interagiert mit dem Controller, um das Modell zu manipulieren
    • Dies kann eine API oder eine Ansicht sein, die Ihre Daten speichert / aktualisiert
    • Dies kann mit request.GET/ request.POST... usw. eingestellt werden
    • Denken Sie auch an Paging oder Filtern .
  • Die Daten aktualisieren die Ansicht
    • Die Vorlagen nehmen die Daten und formatieren sie entsprechend
    • APIs, auch ohne Vorlagen, sind Teil der Ansicht. zB tastypieoderpiston
    • Dies sollte auch die Middleware berücksichtigen.

Nutzen Sie Middleware / Templatetags

  • Wenn Sie für jede Anforderung etwas Arbeit erledigen müssen, ist Middleware eine Möglichkeit.
    • zB Zeitstempel hinzufügen
    • zB Aktualisieren von Metriken über Seitentreffer
    • zB einen Cache füllen
  • Wenn Sie Codeausschnitte haben, die beim Formatieren von Objekten immer wieder vorkommen, sind Vorlagen-Tags gut.
    • zB aktive Tab / URL-Breadcrumbs

Nutzen Sie Modellmanager

  • Erstellen Userkann in einem gehen UserManager(models.Manager).
  • blutige Details für Instanzen sollten auf der models.Model.
  • blutige Details für querysetkönnten in a gehen models.Manager.
  • Möglicherweise möchten Sie jeweils ein Usereinzelnes erstellen , sodass Sie möglicherweise der Meinung sind, dass es auf dem Modell selbst basieren sollte. Beim Erstellen des Objekts verfügen Sie jedoch wahrscheinlich nicht über alle Details:

Beispiel:

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

Verwenden Sie nach Möglichkeit Formulare

Wenn Sie Formulare haben, die einem Modell zugeordnet sind, kann viel Boilerplate-Code entfernt werden. Das ModelForm documentationist ziemlich gut. Das Trennen von Code für Formulare vom Modellcode kann hilfreich sein, wenn Sie viele Anpassungen vornehmen (oder manchmal zyklische Importfehler für erweiterte Verwendungszwecke vermeiden).

Verwenden Sie nach Möglichkeit Verwaltungsbefehle

  • z.B yourapp/management/commands/createsuperuser.py
  • z.B yourapp/management/commands/activateinbulk.py

Wenn Sie über Geschäftslogik verfügen, können Sie diese trennen

  • django.contrib.auth verwendet Backends , genau wie db ein Backend hat ... etc.
  • fügen Sie eine settingfür Ihre Business - Logik (zB AUTHENTICATION_BACKENDS)
  • Du könntest benutzen django.contrib.auth.backends.RemoteUserBackend
  • Du könntest benutzen yourapp.backends.remote_api.RemoteUserBackend
  • Du könntest benutzen yourapp.backends.memcached.RemoteUserBackend
  • Delegieren Sie die schwierige Geschäftslogik an das Backend
  • Stellen Sie sicher, dass die Erwartung direkt am Ein- / Ausgang eingestellt ist.
  • Das Ändern der Geschäftslogik ist so einfach wie das Ändern einer Einstellung :)

Backend-Beispiel:

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' 

könnte werden:

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

mehr über Designmuster

mehr über Schnittstellengrenzen

  • Ist der Code, den Sie verwenden möchten, wirklich Teil der Modelle? ->yourapp.models
  • Ist der Code Teil der Geschäftslogik? ->yourapp.vendor
  • Ist der Code Teil generischer Tools / Bibliotheken? ->yourapp.libs
  • Ist der Code Teil der Geschäftslogikbibliotheken? -> yourapp.libs.vendoroderyourapp.vendor.libs
  • Hier ist eine gute: Können Sie Ihren Code unabhängig testen?
    • ja gut :)
    • Nein, möglicherweise liegt ein Schnittstellenproblem vor
    • Wenn es eine klare Trennung gibt, sollte Unittest mit der Verwendung von Spott ein Kinderspiel sein
  • Ist die Trennung logisch?
    • ja gut :)
    • Nein, möglicherweise haben Sie Probleme, diese logischen Konzepte separat zu testen.
  • Denken Sie, dass Sie umgestalten müssen, wenn Sie 10x mehr Code erhalten?
    • Ja, nein gut, nein bueno, Refactor könnte eine Menge Arbeit sein
    • Nein, das ist einfach großartig!

Kurz gesagt, Sie könnten haben

  • 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

oder irgendetwas anderes, das dir hilft; Das Finden der benötigten Schnittstellen und Grenzen hilft Ihnen dabei.

dnozay
quelle
27

Django verwendet eine leicht modifizierte Art von MVC. In Django gibt es kein Konzept für einen "Controller". Der nächstgelegene Proxy ist eine "Ansicht", die zu Verwechslungen mit MVC-Konvertierungen führt, da eine Ansicht in MVC eher der "Vorlage" von Django ähnelt.

In Django ist ein "Modell" nicht nur eine Datenbankabstraktion. In mancher Hinsicht teilt es die Pflicht mit der "Ansicht" des Django als Controller von MVC. Es enthält das gesamte Verhalten einer Instanz. Wenn diese Instanz als Teil ihres Verhaltens mit einer externen API interagieren muss, ist dies immer noch Modellcode. Tatsächlich müssen Modelle überhaupt nicht mit der Datenbank interagieren, sodass Sie sich Modelle vorstellen können, die vollständig als interaktive Ebene für eine externe API vorhanden sind. Es ist ein viel freieres Konzept eines "Modells".

Chris Pratt
quelle
7

In Django unterscheidet sich die MVC-Struktur, wie Chris Pratt sagte, von dem klassischen MVC-Modell, das in anderen Frameworks verwendet wird. Ich denke, der Hauptgrund dafür ist die Vermeidung einer zu strengen Anwendungsstruktur, wie dies in anderen MVC-Frameworks wie CakePHP der Fall ist.

In Django wurde MVC folgendermaßen implementiert:

Die Ansichtsebene ist zweigeteilt. Die Ansichten sollten nur zum Verwalten von HTTP-Anforderungen verwendet werden. Sie werden aufgerufen und antworten darauf. Ansichten kommunizieren mit dem Rest Ihrer Anwendung (Formulare, Modellformulare, benutzerdefinierte Klassen, in einfachen Fällen direkt mit Modellen). Um die Schnittstelle zu erstellen, verwenden wir Vorlagen. Vorlagen sind für Django stringartig, sie ordnen ihnen einen Kontext zu, und dieser Kontext wurde der Ansicht von der Anwendung mitgeteilt (wenn die Ansicht dies erfordert).

Die Modellebene bietet Kapselung, Abstraktion, Validierung und Intelligenz und macht Ihre Daten objektorientiert (sie sagen, dass DBMS dies eines Tages auch tun wird). Dies bedeutet nicht, dass Sie große models.py-Dateien erstellen sollten (in der Tat ist es ein sehr guter Rat, Ihre Modelle in verschiedene Dateien aufzuteilen, sie in einen Ordner namens 'models' zu legen und eine '__init__.py'-Datei darin zu erstellen Ordner, in den Sie alle Ihre Modelle importieren und schließlich das Attribut 'app_label' der models.Model-Klasse verwenden. Das Modell sollte Sie vom Umgang mit Daten abhalten, da dies Ihre Anwendung vereinfacht. Bei Bedarf sollten Sie auch externe Klassen wie "Werkzeuge" für Ihre Modelle erstellen. Sie können auch das Erbe in Modellen verwenden und das Attribut "abstrakt" der Meta-Klasse Ihres Modells auf "Wahr" setzen.

Wo ist der Rest? Nun, kleine Webanwendungen sind im Allgemeinen eine Art Schnittstelle zu Daten. In einigen kleinen Programmfällen würde es ausreichen, Ansichten zum Abfragen oder Einfügen von Daten zu verwenden. In häufigeren Fällen werden Formulare oder ModelForms verwendet, die eigentlich "Controller" sind. Dies ist nichts anderes als eine praktische und sehr schnelle Lösung für ein häufiges Problem. Es ist das, was eine Website verwendet.

Wenn Formulare für Sie nicht ausreichend sind, sollten Sie Ihre eigenen Klassen erstellen, um die Magie auszuführen. Ein sehr gutes Beispiel hierfür ist die Administratoranwendung: Sie können ModelAmin-Code lesen, dies funktioniert tatsächlich als Controller. Es gibt keine Standardstruktur. Ich empfehle Ihnen, vorhandene Django-Apps zu untersuchen. Dies hängt von jedem Fall ab. Dies ist, was Django-Entwickler beabsichtigten. Sie können eine XML-Parser-Klasse, eine API-Connector-Klasse, Sellerie für die Ausführung von Aufgaben hinzufügen, für eine reaktorbasierte Anwendung verdreht, nur das ORM verwenden, einen Webdienst erstellen, die Administratoranwendung ändern und vieles mehr. Es liegt in Ihrer Verantwortung, Code von guter Qualität zu erstellen, die MVC-Philosophie zu respektieren oder nicht, ihn modulbasiert zu gestalten und Ihre eigenen Abstraktionsschichten zu erstellen. Es ist sehr flexibel.

Mein Rat: Lies so viel Code wie möglich, es gibt viele Django-Anwendungen, aber nimm sie nicht so ernst. Jeder Fall ist anders, Muster und Theorie helfen, aber nicht immer, dies ist eine ungenaue Wissenschaft. Django bietet Ihnen nur gute Werkzeuge, mit denen Sie einige Probleme lindern können (wie Administrationsoberfläche, Validierung von Webformularen, i18n, Implementierung von Beobachtermustern usw.) die zuvor erwähnten und andere), aber gute Designs kommen von erfahrenen Designern.

PS.: Verwenden Sie die Klasse 'User' aus der Auth-Anwendung (aus dem Standard-Django). Sie können beispielsweise Benutzerprofile erstellen oder zumindest den Code lesen. Dies ist für Ihren Fall hilfreich.

Nate Gentile
quelle
1

Eine alte Frage, aber ich möchte trotzdem meine Lösung anbieten. Es basiert auf der Annahme, dass auch Modellobjekte zusätzliche Funktionen erfordern, während es schwierig ist, sie in der Datei models.py zu platzieren . Schwere Geschäftslogik kann je nach persönlichem Geschmack separat geschrieben werden, aber ich mag es zumindest, wenn das Modell alles macht, was mit sich selbst zu tun hat. Diese Lösung unterstützt auch diejenigen, die die gesamte Logik in den Modellen selbst platzieren möchten.

Aus diesem Grund habe ich einen Hack entwickelt , mit dem ich Logik von Modelldefinitionen trennen und trotzdem alle Hinweise von meiner IDE erhalten kann.

Die Vorteile sollten offensichtlich sein, aber dies listet einige auf, die ich beobachtet habe:

  • DB-Definitionen bleiben genau das - kein logischer "Müll" angehängt
  • Die modellbezogene Logik ist ordentlich an einem Ort platziert
  • Alle Dienste (Formulare, REST, Ansichten) haben einen einzigen Zugriffspunkt auf die Logik
  • Das Beste von allem: Ich musste keinen Code neu schreiben, als ich merkte, dass meine models.py zu überladen war und die Logik trennen musste. Die Trennung ist glatt und iterativ: Ich könnte eine Funktion gleichzeitig oder die gesamte Klasse oder die gesamten Modelle ausführen.

Ich habe dies mit Python 3.4 und höher und Django 1.8 und höher verwendet.

app / models.py

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

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

App / Logik / Benutzer.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 ...

Das einzige, was ich nicht herausfinden kann, ist, wie ich meine IDE (in diesem Fall PyCharm) erkennen kann, dass UserLogic tatsächlich ein Benutzermodell ist. Aber da dies offensichtlich ein Hack ist, bin ich ziemlich glücklich, das kleine Ärgernis zu akzeptieren, immer Typ für selfParameter anzugeben .

velis
quelle
Eigentlich sehe ich es als einen einfach zu verwendenden Ansatz. Aber ich würde das endgültige Modell in eine andere Datei verschieben und nicht in models.py erben. Es wäre, als ob service.py Clash Userlogic + Model
Maks
1

Ich würde dir zustimmen müssen. Es gibt viele Möglichkeiten in Django, aber der beste Ausgangspunkt ist die Überprüfung der Designphilosophie von Django .

  1. Das Aufrufen einer API aus einer Modelleigenschaft wäre nicht ideal. Es scheint sinnvoller zu sein, in der Ansicht so etwas zu tun und möglicherweise eine Service-Schicht zu erstellen, um die Dinge trocken zu halten. Wenn der Aufruf der API nicht blockiert und der Aufruf teuer ist, kann es sinnvoll sein, die Anforderung an einen Servicemitarbeiter (einen Mitarbeiter, der aus einer Warteschlange verwendet) zu senden.

  2. Gemäß der Designphilosophie von Django kapseln Modelle jeden Aspekt eines "Objekts". Daher sollte die gesamte Geschäftslogik, die sich auf dieses Objekt bezieht, dort leben:

Schließen Sie alle relevanten Domänenlogiken ein

Modelle sollten jeden Aspekt eines „Objekts“ nach dem Entwurfsmuster von Martin Fowler für Active Record kapseln.

  1. Die von Ihnen beschriebenen Nebenwirkungen sind offensichtlich. Die Logik hier könnte besser in Querysets und Manager unterteilt werden. Hier ist ein Beispiel:

    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
quelle
0

Ich stimme der gewählten Antwort größtenteils zu ( https://stackoverflow.com/a/12857584/871392) ), möchte jedoch eine Option im Abschnitt "Abfragen erstellen" hinzufügen.

Man kann QuerySet-Klassen für Modelle definieren, um Filterabfragen zu erstellen und so weiter. Danach können Sie diese Queryset-Klasse für den Manager des Modells als Proxy verwenden, wie dies bei den integrierten Manager- und QuerySet-Klassen der Fall ist.

Wenn Sie jedoch mehrere Datenmodelle abfragen mussten, um ein Domänenmodell zu erhalten, erscheint es mir vernünftiger, dies wie zuvor vorgeschlagen in ein separates Modul zu stellen.

l0ki
quelle
0

Umfassendster Artikel zu den verschiedenen Optionen mit Vor- und Nachteilen:

  1. Idee Nr. 1: Fette Models
  2. Idee Nr. 2: Einfügen von Geschäftslogik in Ansichten / Formulare
  3. Idee Nr. 3: Dienstleistungen
  4. Idee 4: QuerySets / Manager
  5. Fazit

Quelle: https://sunscrapers.com/blog/where-to-put-business-logic-django/

FSE
quelle
Sie sollten eine Erklärung hinzufügen.
m02ph3u5
-6

Django ist so konzipiert, dass es einfach zur Bereitstellung von Webseiten verwendet werden kann. Wenn Sie sich damit nicht wohl fühlen, sollten Sie vielleicht eine andere Lösung verwenden.

Ich schreibe die Root- oder allgemeinen Operationen auf dem Modell (um dieselbe Schnittstelle zu haben) und die anderen auf dem Controller des Modells. Wenn ich eine Operation von einem anderen Modell benötige, importiere ich dessen Controller.

Dieser Ansatz reicht mir und der Komplexität meiner Anwendungen.

Die Antwort von Hedde ist ein Beispiel, das die Flexibilität von Django und Python selbst zeigt.

Sehr interessante Frage trotzdem!

pvilas
quelle
9
Wie sagt man, dass es gut genug für Sie ist, um die Frage besser zu verstehen?
Chris Wesseling
1
Django hat neben django.db.models noch viel mehr zu bieten, aber der größte Teil des Ökosystems hängt stark von Ihrem Modell ab, das die Django-Modelle verwendet.
Andho
1
Das Entwurfsmuster für die Entwicklung von Software. Und Django wurde entwickelt, um einfach Software im mittleren oder großen Maßstab bereitzustellen, nicht nur Webseiten!
Mohammad Torkashvand