Separazione della logica aziendale e dell'accesso ai dati in django


484

Sto scrivendo un progetto in Django e vedo che l'80% del codice è nel file models.py. Questo codice è confuso e, dopo un certo periodo, smetto di capire cosa sta realmente accadendo.

Ecco cosa mi disturba:

  1. Trovo brutto che il mio livello di modello (che doveva essere responsabile solo del lavoro con i dati di un database) stia anche inviando e-mail, camminando su API ad altri servizi, ecc.
  2. Inoltre, trovo inaccettabile posizionare la logica aziendale nella vista, perché in questo modo diventa difficile da controllare. Ad esempio, nella mia applicazione ci sono almeno tre modi per creare nuove istanze di User, ma tecnicamente dovrebbe crearle in modo uniforme.
  3. Non noto sempre quando i metodi e le proprietà dei miei modelli diventano non deterministici e quando sviluppano effetti collaterali.

Qui c'è un semplice esempio. Inizialmente, il Usermodello era così:

class User(db.Models):

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

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

Nel tempo, si è trasformato in questo:

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

Quello che voglio è separare le entità nel mio codice:

  1. Entità del mio database, a livello di database: cosa contiene la mia applicazione?
  2. Entità della mia applicazione, livello di logica aziendale: cosa può rendere la mia applicazione?

Quali sono le buone pratiche per attuare un tale approccio che può essere applicato in Django?


14
Leggi i segnali
Konstant,

1
bene hai rimosso il tag ma potresti usare DCI per comprendere la separazione di ciò che fa il sistema (la funzionalità) e di ciò che il sistema è (il modello dati / dominio)
Rune FS

2
Proponete di implementare tutta la logica aziendale nei callback di segnale? Sfortunatamente, non tutte le mie applicazioni possono essere collegate ad eventi nel database.
defuz,

Rune FS, ho provato a usare il DCI, ma mi è sembrato che non avesse bisogno di molto per il mio progetto: contesto, definizione di ruoli come mixin agli oggetti, ecc. C'è un modo più semplice di separazione "fa" e " è"? Potresti fare un esempio minimo?
defuz,

Risposte:


635

Sembra che ti stia chiedendo la differenza tra il modello di dati e il modello di dominio : quest'ultimo è dove puoi trovare la logica aziendale e le entità percepite dal tuo utente finale, il primo è dove memorizzi effettivamente i tuoi dati.

Inoltre, ho interpretato la terza parte della tua domanda come: come notare il fallimento nel mantenere separati questi modelli.

Questi sono due concetti molto diversi ed è sempre difficile tenerli separati. Tuttavia, ci sono alcuni modelli e strumenti comuni che possono essere utilizzati per questo scopo.

Informazioni sul modello di dominio

La prima cosa che devi riconoscere è che il tuo modello di dominio non riguarda davvero i dati; si tratta di azioni e domande come "attiva questo utente", "disattiva questo utente", "quali utenti sono attualmente attivati?" e "qual è il nome di questo utente?". In termini classici: si tratta di query e comandi .

Pensare nei comandi

Cominciamo guardando i comandi nel tuo esempio: "attiva questo utente" e "disattiva questo utente". La cosa bella dei comandi è che possono essere facilmente espressi da piccoli scenari dati quando-allora:

dato un utente inattivo
quando l'amministratore attiva questo utente
quindi l'utente diventa attiva
e una e-mail di conferma viene inviata all'utente
e viene aggiunta una voce al registro di sistema
(ecc ecc)

Tali scenari sono utili per vedere come diverse parti della tua infrastruttura possono essere influenzate da un singolo comando - in questo caso il tuo database (una sorta di flag 'attivo'), il tuo server di posta, il tuo registro di sistema, ecc.

Tale scenario ti aiuta davvero anche nella creazione di un ambiente di sviluppo guidato dai test.

Infine, pensare nei comandi ti aiuta davvero a creare un'applicazione orientata alle attività. I tuoi utenti lo apprezzeranno :-)

Esprimere i comandi

Django offre due semplici modi per esprimere i comandi; sono entrambe opzioni valide e non è insolito mescolare i due approcci.

Il livello di servizio

Il modulo di servizio è già stato descritto da @Hedde . Qui si definisce un modulo separato e ogni comando è rappresentato come una funzione.

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

Utilizzando i moduli

L'altro modo è usare un Django Form per ogni comando. Preferisco questo approccio, perché combina molteplici aspetti strettamente correlati:

  • esecuzione del comando (cosa fa?)
  • validazione dei parametri di comando (può farlo?)
  • presentazione del comando (come posso fare questo?)

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

Pensando nelle query

Il tuo esempio non conteneva alcuna domanda, quindi mi sono preso la libertà di fare alcune utili domande. Preferisco usare il termine "domanda", ma query è la terminologia classica. Interessanti query sono: "Qual è il nome di questo utente?", "L'utente può accedere?", "Mostrami un elenco di utenti disattivati" e "Qual è la distribuzione geografica degli utenti disattivati?"

Prima di iniziare a rispondere a queste query, dovresti sempre porti due domande: è una query di presentazione solo per i miei modelli e / o una query di logica aziendale legata all'esecuzione dei miei comandi e / o una query di report .

Le query di presentazione sono fatte semplicemente per migliorare l'interfaccia utente. Le risposte alle query della logica di business influiscono direttamente sull'esecuzione dei comandi. Le query di reporting sono puramente a scopo analitico e presentano vincoli temporali più ampi. Queste categorie non si escludono a vicenda.

L'altra domanda è: "ho il controllo completo sulle risposte?" Ad esempio, quando si esegue una query sul nome dell'utente (in questo contesto) non abbiamo alcun controllo sul risultato, poiché ci affidiamo a un'API esterna.

Fare query

La query più semplice in Django è l'uso dell'oggetto Manager:

User.objects.filter(active=True)

Naturalmente, questo funziona solo se i dati sono effettivamente rappresentati nel modello di dati. Questo non è sempre il caso. In questi casi, puoi considerare le opzioni di seguito.

Tag e filtri personalizzati

La prima alternativa è utile per le query che sono puramente di presentazione: tag personalizzati e filtri modello.

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)

Metodi di query

Se la tua query non è semplicemente di presentazione, puoi aggiungere query al tuo services.py (se lo stai usando), o introdurre un modulo 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 

Modelli proxy

I modelli proxy sono molto utili nel contesto della logica aziendale e del reporting. Fondamentalmente definisci un sottoinsieme migliorato del tuo modello. È possibile ignorare il QuerySet di base di un Manager ignorando il Manager.get_queryset()metodo.

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

Modelli di query

Per query intrinsecamente complesse, ma eseguite abbastanza spesso, esiste la possibilità di modelli di query. Un modello di query è una forma di denormalizzazione in cui i dati rilevanti per una singola query sono memorizzati in un modello separato. Il trucco ovviamente è mantenere il modello denormalizzato in sincronia con il modello primario. I modelli di query possono essere utilizzati solo se le modifiche sono interamente sotto il tuo controllo.

models.py

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

La prima opzione è aggiornare questi modelli nei tuoi comandi. Questo è molto utile se questi modelli sono cambiati solo da uno o due comandi.

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

Un'opzione migliore sarebbe quella di utilizzare segnali personalizzati. Questi segnali sono ovviamente emessi dai tuoi comandi. I segnali hanno il vantaggio di poter sincronizzare più modelli di query con il modello originale. Inoltre, l'elaborazione del segnale può essere scaricata su attività in background, utilizzando sedano o framework simili.

signals.py

user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])

forms.py

class ActivateUserForm(forms.Form):
    # see above

    def execute(self):
        # see above
        user_activated.send_robust(sender=self, user=user)

models.py

class InactiveUserDistribution(models.Model):
    # see above

@receiver(user_activated)
def on_user_activated(sender, **kwargs):
        user = kwargs['user']
        query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
        query_model.inactive_user_count -= 1
        query_model.save()

Mantenerlo pulito

Quando si utilizza questo approccio, diventa ridicolmente facile determinare se il codice rimane pulito. Segui queste linee guida:

  • Il mio modello contiene metodi che fanno qualcosa in più rispetto alla gestione dello stato del database? Dovresti estrarre un comando.
  • Il mio modello contiene proprietà che non si associano ai campi del database? È necessario estrarre una query.
  • Il mio modello fa riferimento a un'infrastruttura che non è il mio database (come la posta)? Dovresti estrarre un comando.

Lo stesso vale per le viste (poiché le viste spesso soffrono dello stesso problema).

  • La mia vista gestisce attivamente i modelli di database? Dovresti estrarre un comando.

Alcuni riferimenti

Documentazione Django: modelli proxy

Documentazione Django: segnali

Architettura: Domain Driven Design


11
È bello vedere una risposta che includa DDD in una domanda relativa al django. Solo perché Django impiega ActiveRecord per la persistenza non significa che la separazione delle preoccupazioni dovrebbe uscire dalla finestra. Bella risposta.
Scott Coates,

6
Se voglio confermare che l'utente looged è il proprietario di un oggetto prima di eliminare quell'oggetto, dovrei verificarlo nella vista o nel modulo modulo / servizio?
Ivan,

6
@Ivan: entrambi. Essa deve essere nel modulo di maschera / servizio, perché è parte del tuo vincoli commerciali. esso dovrebbe anche essere in vista, perché si dovrebbe solo le azioni presenti che gli utenti possono effettivamente eseguire.
publysher,

4
Gestore personalizzato metodi sono un buon modo per implementare query: User.objects.inactive_users(). Ma l'esempio del modello proxy qui IMO porta a una semantica errata: u = InactiveUser.objects.all()[0]; u.active = True; u.save()e ancora isinstance(u, InactiveUser) == True. Vorrei anche menzionare un modo efficace per mantenere un modello di query in molti casi è con una vista db.
Aryeh Leib Taurog

1
@adnanmuttaleb Questo è corretto. Si noti che la risposta stessa utilizza solo il termine "Modello di dominio". Ho incluso il link a DDD non perché la mia risposta è DDD, ma perché quel libro fa un ottimo lavoro nell'aiutarti a pensare ai modelli di dominio.
publysher

148

Di solito implemento un livello di servizio tra viste e modelli. Questo si comporta come l'API del tuo progetto e ti dà una buona visione in elicottero di ciò che sta succedendo. Ho ereditato questa pratica da un mio collega che utilizza molto questa tecnica di stratificazione con progetti Java (JSF), ad esempio:

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

Intendiamoci, di solito prendo modelli, viste e servizi a livello di modulo e li separo ancora di più a seconda delle dimensioni del progetto


8
Mi piace l'approccio generale anche se dalla mia comprensione il tuo esempio specifico di solito verrebbe implementato come manager .
arie,

9
@arie non necessariamente, forse un esempio migliore, per i servizi di un negozio online includere cose come la generazione di sessioni carrello, attività asincrone come i calcoli delle valutazioni dei prodotti, la creazione e l'invio di e-mail eccetera
Hedde van der Heide

4
Mi piace anche questo approccio, proveniente anche da Java. Sono nuovo di Python, come testeresti views.py? Come derideresti il ​​livello di servizio (se, ad esempio, il servizio effettua alcune chiamate API remote)?
Teimuraz,

71

Prima di tutto, non ripeterti .

Quindi, fai attenzione a non ingegnerizzare troppo, a volte è solo una perdita di tempo e fa perdere la concentrazione su ciò che è importante. Rivedi lo zen di Python .

Dai un'occhiata ai progetti attivi

  • più persone = più devono organizzarsi correttamente
  • il repository django hanno una struttura semplice.
  • il repository pip ha una struttura di directory semplice.
  • anche il repository fabric è buono da guardare.

    • puoi posizionare tutti i tuoi modelli sotto yourapp/models/logicalgroup.py
  • ad esempio User, Groupe i modelli correlati possono andare sottoyourapp/models/users.py
  • ad es Poll. Question, Answer... potrebbe andare sottoyourapp/models/polls.py
  • carica quello che ti serve __all__dentroyourapp/models/__init__.py

Maggiori informazioni su MVC

  • modello sono i tuoi dati
    • questo include i tuoi dati effettivi
    • questo include anche i dati di sessione / cookie / cache / fs / indice
  • l'utente interagisce con il controller per manipolare il modello
    • questa potrebbe essere un'API o una vista che salva / aggiorna i tuoi dati
    • questo può essere sintonizzato con request.GET/ request.POST... ecc
    • pensa anche al paging o al filtro .
  • i dati aggiornano la vista
    • i modelli prendono i dati e li formattano di conseguenza
    • Anche le API senza modelli fanno parte della vista; es. tastypieopiston
    • questo dovrebbe anche tenere conto del middleware.

Approfitta dei middleware / templatetags

  • Se è necessario eseguire del lavoro per ogni richiesta, il middleware è un modo per procedere.
    • ad es. aggiunta di timestamp
    • ad es. aggiornamento delle metriche sui risultati delle pagine
    • ad es. popolare una cache
  • Se hai frammenti di codice che ricorrono sempre per la formattazione di oggetti, i tag di templat sono validi.
    • ad es. scheda attiva / URL breadcrumb

Approfitta dei gestori di modelli

  • la creazione Userpuò andare in a UserManager(models.Manager).
  • i dettagli gory per le istanze dovrebbero andare sul models.Model.
  • i dettagli gory per querysetpotrebbero andare in a models.Manager.
  • potresti volerne creare Useruno alla volta, quindi potresti pensare che dovrebbe vivere sul modello stesso, ma quando crei l'oggetto, probabilmente non hai tutti i dettagli:

Esempio:

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

Utilizzare i moduli ove possibile

È possibile eliminare un sacco di codice del boilerplate se si dispone di moduli associati a un modello. Il ModelForm documentationè abbastanza buono. La separazione del codice per i moduli dal codice modello può essere utile se si dispone di molta personalizzazione (o talvolta si evitano errori di importazione ciclici per usi più avanzati).

Utilizzare i comandi di gestione quando possibile

  • per esempio yourapp/management/commands/createsuperuser.py
  • per esempio yourapp/management/commands/activateinbulk.py

se si dispone di una logica aziendale, è possibile separarla

  • django.contrib.auth usa i backend , proprio come db ha un backend ... ecc.
  • aggiungi un settingper la tua logica aziendale (ad es. AUTHENTICATION_BACKENDS)
  • potresti usare django.contrib.auth.backends.RemoteUserBackend
  • potresti usare yourapp.backends.remote_api.RemoteUserBackend
  • potresti usare yourapp.backends.memcached.RemoteUserBackend
  • delegare la difficile logica aziendale al back-end
  • assicurarsi di impostare le aspettative proprio sull'ingresso / uscita.
  • cambiare la logica aziendale è semplice come cambiare un'impostazione :)

esempio di backend:

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' 

potrebbe diventare:

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

altro sui modelli di design

altro sui confini dell'interfaccia

  • Il codice che vuoi usare fa davvero parte dei modelli? ->yourapp.models
  • Il codice fa parte della logica aziendale? ->yourapp.vendor
  • Il codice fa parte di strumenti / librerie generici? ->yourapp.libs
  • Il codice fa parte delle librerie di logica aziendale? -> yourapp.libs.vendoroyourapp.vendor.libs
  • Eccone uno buono: puoi testare il tuo codice in modo indipendente?
    • si bene :)
    • no, potresti avere un problema di interfaccia
    • quando c'è una chiara separazione, unittest dovrebbe essere un gioco da ragazzi con l'uso di derisione
  • La separazione è logica?
    • si bene :)
    • no, potresti avere difficoltà a testare questi concetti logici separatamente.
  • Pensi che dovrai refactoring quando ricevi 10 volte più codice?
    • sì, non va bene, no bueno, il refactor potrebbe richiedere molto lavoro
    • no, è semplicemente fantastico!

Insomma, potresti averlo

  • 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

o qualsiasi altra cosa che ti aiuti; trovare le interfacce di cui hai bisogno e i confini ti aiuteranno.


27

Django impiega un tipo leggermente modificato di MVC. Non esiste il concetto di "controller" in Django. Il proxy più vicino è una "vista", che tende a creare confusione con i convertitori MVC perché in MVC una vista è più simile al "modello" di Django.

In Django, un "modello" non è semplicemente un'astrazione di database. Per alcuni aspetti, condivide il dovere con la "visione" di Django come controller di MVC. Contiene l'intero comportamento associato a un'istanza. Se tale istanza deve interagire con un'API esterna come parte del suo comportamento, questo è comunque il codice modello. In effetti, i modelli non sono tenuti ad interagire affatto con il database, quindi è possibile concepire modelli che esistono interamente come layer interattivi per un'API esterna. È un concetto molto più libero di un "modello".


7

A Django, la struttura MVC è come diceva Chris Pratt, diverso dal classico modello MVC utilizzato in altri framework, penso che il motivo principale per farlo sia evitare una struttura applicativa troppo rigida, come accade in altri framework MVC come CakePHP.

A Django, MVC è stato implementato nel modo seguente:

Il livello di visualizzazione è diviso in due. Le viste devono essere utilizzate solo per gestire le richieste HTTP, vengono chiamate e rispondono ad esse. Le viste comunicano con il resto dell'applicazione (moduli, moduli modello, classi personalizzate, in casi semplici direttamente con i modelli). Per creare l'interfaccia usiamo i modelli. I modelli sono simili a stringhe per Django, in essi viene mappato un contesto e questo contesto è stato comunicato alla vista dall'applicazione (quando la vista lo richiede).

Il livello del modello fornisce incapsulamento, astrazione, convalida, intelligenza e rende i tuoi dati orientati agli oggetti (dicono che un giorno anche DBMS lo farà). Questo non significa che dovresti creare enormi file models.py (in realtà un ottimo consiglio è quello di dividere i tuoi modelli in file diversi, metterli in una cartella chiamata 'modelli', creare un file '__init__.py' in questo cartella in cui si importano tutti i modelli e infine si utilizza l'attributo 'etichetta_app' di models.Model class). Il modello dovrebbe sottrarti al funzionamento con i dati, renderà la tua applicazione più semplice. Se necessario, dovresti anche creare classi esterne, come "strumenti" per i tuoi modelli. Puoi anche usare l'eredità nei modelli, impostando l'attributo 'astratto' della classe Meta del tuo modello su 'Vero'.

Dov'è il resto? Bene, le piccole applicazioni web in genere sono una sorta di interfaccia per i dati, in alcuni casi di programmi di piccole dimensioni sarebbe sufficiente usare le viste per interrogare o inserire dati. Casi più comuni useranno Forms o ModelForms, che in realtà sono "controller". Questa non è altro che una soluzione pratica a un problema comune e molto veloce. È ciò che un sito Web usa per fare.

Se i moduli non sono enogh per te, allora dovresti creare le tue classi per fare la magia, un ottimo esempio di questo è l'applicazione di amministrazione: puoi leggere il codice ModelAmin, questo funziona effettivamente come un controller. Non esiste una struttura standard, ti consiglio di esaminare le app Django esistenti, dipende da ogni caso. Questo è ciò che intendevano gli sviluppatori Django, è possibile aggiungere una classe parser xml, una classe connettore API, aggiungere sedano per eseguire attività, distorto per un'applicazione basata su un reattore, utilizzare solo ORM, creare un servizio Web, modificare l'applicazione di amministrazione e altro ancora. .. È tua responsabilità fare codice di buona qualità, rispettare o meno la filosofia MVC, renderlo basato su modulo e creare i tuoi livelli di astrazione. È molto flessibile.

Il mio consiglio: leggi tutto il codice che puoi, ci sono molte applicazioni django in giro, ma non prenderle così sul serio. Ogni caso è diverso, i modelli e la teoria aiutano, ma non sempre, questa è una cienza imprecisa, django ti fornisce solo buoni strumenti che puoi usare per alleviare alcuni dolori (come l'interfaccia di amministrazione, la convalida del modulo web, i18n, l'implementazione del modello di osservatore, tutto il già citato e altri), ma i buoni progetti provengono da designer esperti.

PS .: usa la classe 'Utente' dall'applicazione auth (dallo standard django), puoi creare ad esempio profili utente, o almeno leggere il suo codice, sarà utile per il tuo caso.


1

Una vecchia domanda, ma vorrei comunque offrire la mia soluzione. È basato sull'accettazione che anche gli oggetti modello richiedono alcune funzionalità aggiuntive mentre è scomodo posizionarlo all'interno di models.py . La logica di business pesante può essere scritta separatamente a seconda dei gusti personali, ma almeno mi piace il modello di fare tutto ciò che è legato a se stesso. Questa soluzione supporta anche coloro a cui piace avere tutta la logica inserita nei modelli stessi.

Come tale, ho ideato un hack che mi consente di separare la logica dalle definizioni del modello e ottenere comunque tutti i suggerimenti dal mio IDE.

I vantaggi dovrebbero essere ovvi, ma questo elenca alcuni che ho osservato:

  • Le definizioni del DB rimangono esattamente questo: nessuna "spazzatura" logica collegata
  • La logica relativa al modello è posizionata ordinatamente in un unico posto
  • Tutti i servizi (moduli, REST, viste) hanno un unico punto di accesso alla logica
  • Soprattutto : non ho dovuto riscrivere alcun codice una volta capito che i miei modelli.py sono diventati troppo ingombranti e ho dovuto separare la logica. La separazione è fluida e iterativa: potrei fare una funzione alla volta o l'intera classe o l'intero modello.

Ho usato questo con Python 3.4 e versioni successive e Django 1.8 e versioni successive.

app / models.py

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

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

app / logica / 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 ...

L'unica cosa che non riesco a capire è come far riconoscere il mio IDE (in questo caso PyCharm) che UserLogic è in realtà un modello utente. Ma dal momento che questo è ovviamente un trucco, sono abbastanza felice di accettare il piccolo fastidio di specificare sempre il tipo per selfparametro.


In realtà lo vedo come un approccio facile da usare. Ma sposterei il modello finale in un altro file e non erediterei in models.py. Sarebbe come service.py erano scontrarsi userlogic + modello
Maks

1

Dovrei essere d'accordo con te. Ci sono molte possibilità nel django ma il miglior punto di partenza è rivedere la filosofia del design di Django .

  1. Chiamare un'API da una proprietà del modello non sarebbe l'ideale, sembra che avrebbe più senso fare qualcosa di simile nella vista e possibilmente creare un livello di servizio per mantenere le cose asciutte. Se la chiamata all'API non è bloccante e la chiamata è costosa, può essere utile inviare la richiesta a un operatore del servizio (un lavoratore che consuma da una coda).

  2. Secondo la filosofia del design di Django, i modelli incapsulano ogni aspetto di un "oggetto". Quindi tutta la logica aziendale relativa a quell'oggetto dovrebbe vivere lì:

Includi tutta la logica di dominio pertinente

I modelli dovrebbero incapsulare ogni aspetto di un "oggetto", seguendo il modello di progettazione Active Record di Martin Fowler.

  1. Gli effetti collaterali che descrivi sono evidenti, la logica qui potrebbe essere meglio suddivisa in Queryset e gestori. Ecco un esempio:

    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

Sono principalmente d'accordo con la risposta scelta ( https://stackoverflow.com/a/12857584/871392 ), ma voglio aggiungere un'opzione nella sezione Esecuzione di query.

È possibile definire le classi QuerySet per i modelli per eseguire query di filtro e così via. Successivamente, è possibile eseguire il proxy di questa classe di query per il gestore del modello, come fanno le classi Manager e QuerySet integrate.

Tuttavia, se per ottenere un modello di dominio è stato necessario interrogare diversi modelli di dati, mi sembra più ragionevole metterlo in un modulo separato come suggerito prima.



-6

Django è progettato per essere facilmente utilizzato per la consegna di pagine Web. Se non ti senti a tuo agio con questo, forse dovresti usare un'altra soluzione.

Sto scrivendo la radice o le operazioni comuni sul modello (per avere la stessa interfaccia) e le altre sul controller del modello. Se ho bisogno di un'operazione da un altro modello, importa il suo controller.

Questo approccio è sufficiente per me e la complessità delle mie applicazioni.

La risposta di Hedde è un esempio che mostra la flessibilità di django e python stessi.

Domanda molto interessante comunque!


9
In che modo dire che è abbastanza buono per te è utile per la mia comprensione della tua domanda?
Chris Wesseling,

1
Django ha molto di più da offrire oltre a django.db.models, ma la maggior parte dell'ecosistema dipende fortemente dal tuo modello usando i modelli django.
andho,

1
Il modello di progettazione utilizzato per lo sviluppo di software. E django progettato per essere facilmente utilizzato per fornire software in media o grande scala non solo pagine Web!
Mohammad Torkashvand,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.