Separación de lógica de negocios y acceso a datos en django


484

Estoy escribiendo un proyecto en Django y veo que el 80% del código está en el archivo models.py. Este código es confuso y, después de cierto tiempo, dejo de entender lo que realmente está sucediendo.

Esto es lo que me molesta:

  1. Me parece feo que mi nivel de modelo (que se suponía que era responsable solo del trabajo con datos de una base de datos) también está enviando correos electrónicos, caminando sobre la API a otros servicios, etc.
  2. Además, me parece inaceptable colocar la lógica de negocios en la vista, porque de esta manera se vuelve difícil de controlar. Por ejemplo, en mi aplicación hay al menos tres formas de crear nuevas instancias User, pero técnicamente debería crearlas de manera uniforme.
  3. No siempre me doy cuenta cuando los métodos y propiedades de mis modelos se vuelven no deterministas y cuando desarrollan efectos secundarios.

Aquí hay un ejemplo simple. Al principio, el Usermodelo era así:

class User(db.Models):

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

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

Con el tiempo, se convirtió en esto:

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

Lo que quiero es separar las entidades en mi código:

  1. Entidades de mi base de datos, nivel de base de datos: ¿Qué contiene mi aplicación?
  2. Entidades de mi aplicación, nivel de lógica de negocios: ¿Qué puede hacer mi aplicación?

¿Cuáles son las buenas prácticas para implementar un enfoque que se pueda aplicar en Django?


14
Lea sobre las señales
Konstant

1
bueno, eliminó la etiqueta, pero podría usar DCI para completar la separación de lo que hace el sistema (la funcionalidad) y lo que es el sistema (el modelo de datos / dominio)
Rune FS

2
¿Propone implementar toda la lógica de negocios en devoluciones de llamada de señal? Desafortunadamente, no toda mi aplicación se puede vincular a eventos en la base de datos.
defuz

Rune FS, traté de usar el DCI, pero me pareció que no necesitaba mucho para mi proyecto: Contexto, definición de roles como mezcla para los objetos, etc. Hay una forma más fácil de separación "hace" y " es"? ¿Podrías dar un ejemplo mínimo?
defuz

Respuestas:


635

Parece que está preguntando sobre la diferencia entre el modelo de datos y el modelo de dominio : en este último es donde puede encontrar la lógica comercial y las entidades tal como las percibe su usuario final, el primero es donde realmente almacena sus datos.

Además, he interpretado la tercera parte de su pregunta como: cómo notar la falla para mantener estos modelos separados.

Estos son dos conceptos muy diferentes y siempre es difícil mantenerlos separados. Sin embargo, existen algunos patrones y herramientas comunes que pueden usarse para este propósito.

Sobre el modelo de dominio

Lo primero que debe reconocer es que su modelo de dominio no se trata realmente de datos; se trata de acciones y preguntas como "activar este usuario", "desactivar este usuario", "¿qué usuarios están activados actualmente?" y "¿cuál es el nombre de este usuario?". En términos clásicos: se trata de consultas y comandos .

Pensar en comandos

Comencemos mirando los comandos en su ejemplo: "activar este usuario" y "desactivar este usuario". Lo bueno de los comandos es que pueden expresarse fácilmente mediante pequeños escenarios de cuándo-entonces:

dado un usuario inactivo
cuando el administrador activa a este usuario
, el usuario se activa
y se envía un correo electrónico de confirmación al usuario
y se agrega una entrada al registro del sistema
(etc., etc.)

Tales escenarios son útiles para ver cómo diferentes partes de su infraestructura pueden verse afectadas por un solo comando, en este caso, su base de datos (algún tipo de bandera 'activa'), su servidor de correo, el registro de su sistema, etc.

Tal escenario también lo ayuda realmente a configurar un entorno de desarrollo dirigido por pruebas.

Y finalmente, pensar en comandos realmente te ayuda a crear una aplicación orientada a tareas. Sus usuarios apreciarán esto :-)

Expresando comandos

Django proporciona dos formas fáciles de expresar comandos; Ambos son opciones válidas y no es inusual mezclar los dos enfoques.

La capa de servicio

El módulo de servicio ya ha sido descrito por @Hedde . Aquí define un módulo separado y cada comando se representa como una función.

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

Usando formularios

La otra forma es usar un formulario Django para cada comando. Prefiero este enfoque, porque combina múltiples aspectos estrechamente relacionados:

  • ejecución del comando (¿qué hace?)
  • validación de los parámetros del comando (¿puede hacer esto?)
  • presentación del comando (¿cómo puedo hacer esto?)

formas.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 en consultas

Su ejemplo no contenía ninguna consulta, así que me tomé la libertad de hacer algunas consultas útiles. Prefiero usar el término "pregunta", pero las consultas es la terminología clásica. Las consultas interesantes son: "¿Cuál es el nombre de este usuario?", "¿Puede este usuario iniciar sesión?", "Mostrarme una lista de usuarios desactivados" y "¿Cuál es la distribución geográfica de los usuarios desactivados?"

Antes de embarcarse en responder estas preguntas, siempre debe hacerse dos preguntas: ¿es esta una consulta de presentación solo para mis plantillas, y / o una consulta de lógica de negocios vinculada a la ejecución de mis comandos, y / o una consulta de informes ?

Las consultas de presentación se realizan simplemente para mejorar la interfaz de usuario. Las respuestas a las consultas de lógica de negocios afectan directamente la ejecución de sus comandos. Las consultas de informes son meramente para fines analíticos y tienen limitaciones de tiempo más flexibles. Estas categorías no son mutuamente excluyentes.

La otra pregunta es: "¿Tengo control completo sobre las respuestas?" Por ejemplo, al consultar el nombre del usuario (en este contexto) no tenemos ningún control sobre el resultado, porque confiamos en una API externa.

Haciendo consultas

La consulta más básica en Django es el uso del objeto Manager:

User.objects.filter(active=True)

Por supuesto, esto solo funciona si los datos están realmente representados en su modelo de datos. Este no es siempre el caso. En esos casos, puede considerar las siguientes opciones.

Etiquetas y filtros personalizados

La primera alternativa es útil para consultas que son simplemente de presentación: etiquetas personalizadas y filtros de plantilla.

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)

Métodos de consulta

Si su consulta no es simplemente de presentación, puede agregar consultas a sus services.py (si está usando eso), o introducir un módulo 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 

Modelos proxy

Los modelos proxy son muy útiles en el contexto de la lógica empresarial y los informes. Básicamente, define un subconjunto mejorado de su modelo. Puede anular el QuerySet base de un Administrador anulando el Manager.get_queryset()método.

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

Modelos de consulta

Para consultas que son inherentemente complejas, pero que se ejecutan con bastante frecuencia, existe la posibilidad de modelos de consulta. Un modelo de consulta es una forma de desnormalización donde los datos relevantes para una sola consulta se almacenan en un modelo separado. El truco, por supuesto, es mantener el modelo desnormalizado sincronizado con el modelo primario. Los modelos de consulta solo se pueden usar si los cambios están completamente bajo su control.

modelos.py

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

La primera opción es actualizar estos modelos en sus comandos. Esto es muy útil si estos modelos solo se cambian con uno o dos comandos.

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

Una mejor opción sería utilizar señales personalizadas. Estas señales son, por supuesto, emitidas por sus comandos. Las señales tienen la ventaja de que puede mantener múltiples modelos de consulta sincronizados con su modelo original. Además, el procesamiento de la señal se puede descargar a tareas en segundo plano, utilizando Celery o marcos similares.

señales.py

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

formas.py

class ActivateUserForm(forms.Form):
    # see above

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

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

Manteniéndolo limpio

Al usar este enfoque, resulta ridículamente fácil determinar si su código se mantiene limpio. Simplemente siga estas pautas:

  • ¿Mi modelo contiene métodos que hacen más que administrar el estado de la base de datos? Deberías extraer un comando.
  • ¿Mi modelo contiene propiedades que no se asignan a los campos de la base de datos? Deberías extraer una consulta.
  • ¿Mi modelo hace referencia a la infraestructura que no es mi base de datos (como el correo)? Deberías extraer un comando.

Lo mismo ocurre con las vistas (porque las vistas a menudo sufren el mismo problema).

  • ¿Mi vista gestiona activamente los modelos de bases de datos? Deberías extraer un comando.

Algunas referencias

Documentación de Django: modelos proxy

Documentación de Django: señales

Arquitectura: diseño dirigido por dominios


11
Es bueno ver una respuesta que incorpora DDD en una pregunta relacionada con django. El hecho de que Django emplea ActiveRecord para la persistencia no significa que la separación de las preocupaciones deba desaparecer. Gran respuesta.
Scott Coates

66
Si quiero validar que el usuario conectado es el propietario de un objeto antes de eliminar ese objeto, ¿debo verificarlo en la vista o en el módulo de formulario / servicio?
Ivan

66
@ Ivan: ambos. Se debe estar en el módulo de formulario / servicio, ya que es parte de las limitaciones de su negocio. También debe estar en la vista porque solo debe presentar acciones que los usuarios realmente puedan ejecutar.
publysher

44
Gestor personalizado métodos son una buena manera de implementar consultas: User.objects.inactive_users(). Pero el ejemplo del modelo de proxy aquí IMO conduce a una semántica incorrecta: u = InactiveUser.objects.all()[0]; u.active = True; u.save()y aún isinstance(u, InactiveUser) == True. También mencionaría que una forma efectiva de mantener un modelo de consulta en muchos casos es con una vista db.
Aryeh Leib Taurog

1
@adnanmuttaleb Esto es correcto. Tenga en cuenta que la respuesta en sí solo usa el término "Modelo de dominio". He incluido el enlace a DDD no porque mi respuesta sea DDD, sino porque ese libro hace un gran trabajo al ayudarlo a pensar en modelos de dominio.
publysher

148

Por lo general, implemento una capa de servicio entre vistas y modelos. Esto actúa como la API de su proyecto y le brinda una buena vista en helicóptero de lo que está sucediendo. Heredé esta práctica de un colega mío que usa mucho esta técnica de estratificación con proyectos Java (JSF), por ejemplo:

modelos.py

class Book:
   author = models.ForeignKey(User)
   title = models.CharField(max_length=125)

   class Meta:
       app_label = "library"

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

Eso sí, generalmente llevo modelos, vistas y servicios a nivel de módulo y los separo aún más dependiendo del tamaño del proyecto.


8
Sin embargo, me gusta el enfoque general, según tengo entendido, su ejemplo específico generalmente se implementaría como gerente .
arie

99
@arie no necesariamente, quizás un mejor ejemplo, para los servicios de una tienda en línea incluiría cosas como generar sesiones de carrito, tareas asincrónicas como cálculos de calificaciones de productos, creación y envío de correos electrónicos, etc.
Hedde van der Heide

44
También me gusta este enfoque, que viene también de Java. Soy nuevo en Python, ¿cómo probarías views.py? ¿Cómo se burlaría de la capa de servicio (si, por ejemplo, el servicio realiza algunas llamadas de API remotas)?
Teimuraz

71

En primer lugar, no te repitas .

Entonces, tenga cuidado de no diseñar demasiado, a veces es solo una pérdida de tiempo y hace que alguien pierda el enfoque en lo que es importante. Revise el zen de Python de vez en cuando.

Echa un vistazo a los proyectos activos.

  • Más personas = más necesidad de organizarse adecuadamente
  • El repositorio de Django tiene una estructura sencilla.
  • En el repositorio pip tienen una estructura de directorio sencilla.
  • el repositorio de telas también es bueno para mirar.

    • puedes colocar todos tus modelos debajo yourapp/models/logicalgroup.py
  • por ejemplo User, Groupy los modelos relacionados pueden ir debajoyourapp/models/users.py
  • por ejemplo Poll, Question, Answer... podría pasar por debajoyourapp/models/polls.py
  • carga lo que necesitas __all__dentro deyourapp/models/__init__.py

Más acerca de MVC

  • modelo es su información
    • esto incluye sus datos reales
    • esto también incluye sus datos de sesión / cookie / caché / fs / index
  • el usuario interactúa con el controlador para manipular el modelo
    • esto podría ser una API o una vista que guarda / actualiza sus datos
    • esto se puede sintonizar con request.GET/ request.POST... etc.
    • piense en paginación o filtrado también.
  • los datos actualizan la vista
    • las plantillas toman los datos y los formatean en consecuencia
    • Las API incluso sin plantillas son parte de la vista; por ejemplo tastypieopiston
    • Esto también debería dar cuenta del middleware.

Aproveche middleware / templatetags

  • Si necesita trabajo por hacer para cada solicitud, el middleware es un camino a seguir.
    • por ejemplo, agregar marcas de tiempo
    • por ejemplo, actualizar métricas sobre visitas a la página
    • Por ejemplo, llenar un caché
  • Si tiene fragmentos de código que siempre se repiten para formatear objetos, las etiquetas de plantilla son buenas.
    • por ejemplo, migas de pan activas de pestaña / url

Aproveche los gerentes de modelo

  • crear Userpuede ir en a UserManager(models.Manager).
  • Los detalles sangrientos para las instancias deben ir al models.Model.
  • detalles sangrientos para querysetpodrían ir en a models.Manager.
  • es posible que desee crear Useruno a la vez, por lo que puede pensar que debería vivir en el modelo en sí, pero al crear el objeto, probablemente no tenga todos los detalles:

Ejemplo:

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

Haga uso de formularios siempre que sea posible

Se puede eliminar una gran cantidad de código repetitivo si tiene formularios que se asignan a un modelo. El ModelForm documentationes bastante bueno. Separar el código de los formularios del código del modelo puede ser bueno si tiene mucha personalización (o, a veces, evita errores de importación cíclicos para usos más avanzados).

Use comandos de administración cuando sea posible

  • p.ej yourapp/management/commands/createsuperuser.py
  • p.ej yourapp/management/commands/activateinbulk.py

Si tiene lógica empresarial, puede separarla

  • django.contrib.auth usa backends , al igual que db tiene un backend ... etc.
  • agregue un settingpara su lógica de negocios (por ejemplo AUTHENTICATION_BACKENDS)
  • podrías usar django.contrib.auth.backends.RemoteUserBackend
  • podrías usar yourapp.backends.remote_api.RemoteUserBackend
  • podrías usar yourapp.backends.memcached.RemoteUserBackend
  • delegar la lógica comercial difícil al backend
  • asegúrese de establecer la expectativa correcta en la entrada / salida.
  • cambiar la lógica de negocios es tan simple como cambiar una configuración :)

ejemplo de 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' 

podría convertirse:

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

Más sobre patrones de diseño

Más información sobre los límites de la interfaz

  • ¿El código que desea usar es realmente parte de los modelos? ->yourapp.models
  • ¿Es el código parte de la lógica empresarial? ->yourapp.vendor
  • ¿Es el código parte de herramientas genéricas / libs? ->yourapp.libs
  • ¿Es el código parte de las bibliotecas de lógica de negocios? -> yourapp.libs.vendoroyourapp.vendor.libs
  • Aquí hay una buena: ¿puedes probar tu código de forma independiente?
    • si bien :)
    • no, es posible que tengas un problema de interfaz
    • cuando hay una separación clara, la prueba de unidad debe ser muy fácil con el uso de burlas
  • ¿Es lógica la separación?
    • si bien :)
    • no, es posible que tenga problemas para probar esos conceptos lógicos por separado.
  • ¿Crees que necesitarás refactorizar cuando obtengas 10 veces más código?
    • sí, no bueno, no bueno, refactorizar podría ser mucho trabajo
    • no, eso es simplemente genial!

En resumen, podrías tener

  • 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 cualquier otra cosa que te ayude; Encontrar las interfaces que necesita y los límites lo ayudarán.


27

Django emplea un tipo de MVC ligeramente modificado. No hay concepto de un "controlador" en Django. El proxy más cercano es una "vista", que tiende a causar confusión con los conversos de MVC porque en MVC una vista se parece más a la "plantilla" de Django.

En Django, un "modelo" no es simplemente una abstracción de la base de datos. En algunos aspectos, comparte el deber con la "visión" de Django como el controlador de MVC. Contiene la totalidad del comportamiento asociado con una instancia. Si esa instancia necesita interactuar con una API externa como parte de su comportamiento, entonces ese sigue siendo el código del modelo. De hecho, no es necesario que los modelos interactúen con la base de datos, por lo que podría concebir modelos que existan completamente como una capa interactiva para una API externa. Es un concepto mucho más libre de un "modelo".


7

En Django, la estructura MVC es, como dijo Chris Pratt, diferente del modelo MVC clásico utilizado en otros marcos, creo que la razón principal para hacerlo es evitar una estructura de aplicación demasiado estricta, como sucede en otros marcos MVC como CakePHP.

En Django, MVC se implementó de la siguiente manera:

La capa de vista se divide en dos. Las vistas solo deben usarse para administrar solicitudes HTTP, se llaman y responden a ellas. Las vistas se comunican con el resto de su aplicación (formularios, formas de modelo, clases personalizadas, o en casos simples directamente con modelos). Para crear la interfaz usamos plantillas. Las plantillas tienen forma de cadena para Django, asigna un contexto en ellas, y la aplicación comunicó este contexto a la vista (cuando la vista pregunta).

La capa de modelo proporciona encapsulación, abstracción, validación, inteligencia y hace que sus datos estén orientados a objetos (dicen que algún día DBMS también lo hará). Esto no significa que deba crear grandes archivos de models.py (de hecho, un muy buen consejo es dividir sus modelos en diferentes archivos, colocarlos en una carpeta llamada 'modelos', hacer un archivo '__init__.py' en este carpeta donde importa todos sus modelos y finalmente usa el atributo 'app_label' de models.Model class). El modelo debe abstraerlo de la operación con datos, hará que su aplicación sea más simple. También debe, si es necesario, crear clases externas, como "herramientas" para sus modelos. También puede usar la herencia en los modelos, estableciendo el atributo 'abstracto' de la clase Meta de su modelo en 'Verdadero'.

¿Donde esta el resto? Bueno, las aplicaciones web pequeñas generalmente son una especie de interfaz para los datos, en algunos casos de programas pequeños usar vistas para consultar o insertar datos sería suficiente. Los casos más comunes utilizarán Formularios o ModelForms, que en realidad son "controladores". Esto no es otra cosa que una solución práctica a un problema común y muy rápido. Es lo que suele hacer un sitio web.

Si los formularios no son suficientes para usted, entonces debe crear sus propias clases para hacer la magia, un muy buen ejemplo de esto es la aplicación de administración: puede leer el código ModelAmin, esto realmente funciona como un controlador. No hay una estructura estándar, le sugiero que examine las aplicaciones existentes de Django, depende de cada caso. Esto es lo que pretendían los desarrolladores de Django, puede agregar una clase de analizador xml, una clase de conector API, agregar Celery para realizar tareas, retorcer para una aplicación basada en reactor, usar solo el ORM, hacer un servicio web, modificar la aplicación de administración y más. .. Es su responsabilidad crear un código de buena calidad, respetar la filosofía MVC o no, hacer que esté basado en módulos y crear sus propias capas de abstracción. Es muy flexible

Mi consejo: lea tanto código como pueda, hay muchas aplicaciones de Django, pero no las tome tan en serio. Cada caso es diferente, los patrones y la teoría ayudan, pero no siempre, esta es una idea imprecisa, django solo le proporciona buenas herramientas que puede usar para aliviar algunos dolores (como interfaz de administrador, validación de formularios web, i18n, implementación de patrones de observador, todos los mencionados anteriormente y otros), pero los buenos diseños provienen de diseñadores experimentados.

PD .: use la clase 'Usuario' de la aplicación de autenticación (de django estándar), puede hacer, por ejemplo, perfiles de usuario, o al menos leer su código, será útil para su caso.


1

Una vieja pregunta, pero me gustaría ofrecer mi solución de todos modos. Se basa en la aceptación de que los objetos modelo también requieren alguna funcionalidad adicional, mientras que es incómodo colocarlo dentro de models.py . La lógica comercial pesada puede escribirse por separado dependiendo del gusto personal, pero al menos me gusta que el modelo haga todo lo relacionado consigo mismo. Esta solución también es compatible con aquellos a quienes les gusta tener toda la lógica colocada dentro de los propios modelos.

Como tal, ideé un truco que me permite separar la lógica de las definiciones del modelo y aún obtener toda la indirecta de mi IDE.

Las ventajas deberían ser obvias, pero esto enumera algunas que he observado:

  • Las definiciones de la base de datos siguen siendo solo eso: no se adjunta ninguna "basura" lógica
  • La lógica relacionada con el modelo se coloca perfectamente en un solo lugar
  • Todos los servicios (formularios, REST, vistas) tienen un único punto de acceso a la lógica.
  • Lo mejor de todo: no tuve que volver a escribir ningún código una vez que me di cuenta de que mis models.py se volvieron demasiado desordenados y tuve que separar la lógica. La separación es suave e iterativa: podría hacer una función a la vez o toda la clase o los modelos completos.py.

He estado usando esto con Python 3.4 y superior y Django 1.8 y superior.

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

Lo único que no puedo entender es cómo hacer que mi IDE (PyCharm en este caso) reconozca que UserLogic es en realidad un modelo de usuario. Pero dado que esto es obviamente un truco, estoy muy contento de aceptar la pequeña molestia de especificar siempre el tipo de selfparámetro.


En realidad, lo veo como un enfoque fácil de usar. Pero movería el modelo final a otro archivo y no heredaría en models.py. Sería como si service.py fuera clash userlogic + model
Maks el

1

Tendre que estar de acuerdo contigo. Hay muchas posibilidades en django, pero el mejor lugar para comenzar es revisar la filosofía de diseño de Django .

  1. Llamar a una API desde una propiedad de modelo no sería ideal, parece que tendría más sentido hacer algo como esto en la vista y posiblemente crear una capa de servicio para mantener las cosas secas. Si la llamada a la API no es de bloqueo y la llamada es costosa, puede tener sentido enviar la solicitud a un trabajador de servicio (un trabajador que consume de una cola).

  2. Según la filosofía de diseño de Django, los modelos encapsulan cada aspecto de un "objeto". Entonces, toda la lógica de negocios relacionada con ese objeto debería vivir allí:

Incluir toda la lógica de dominio relevante

Los modelos deben encapsular cada aspecto de un "objeto", siguiendo el patrón de diseño de Registro Activo de Martin Fowler.

  1. Los efectos secundarios que describe son evidentes, la lógica aquí podría dividirse mejor en conjuntos de consultas y administradores. Aquí hay un ejemplo:

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

Principalmente estoy de acuerdo con la respuesta elegida ( https://stackoverflow.com/a/12857584/871392 ), pero quiero agregar la opción en la sección Hacer consultas.

Se pueden definir clases de QuerySet para modelos para realizar consultas de filtro, etc. Después de eso, puede proxy esta clase de conjunto de consultas para el administrador del modelo, como lo hacen las clases Build-in Manager y QuerySet.

Sin embargo, si tuviera que consultar varios modelos de datos para obtener un modelo de dominio, me parece más razonable poner esto en un módulo separado como se sugirió anteriormente.



-6

Django está diseñado para ser utilizado fácilmente para entregar páginas web. Si no se siente cómodo con esto, quizás debería usar otra solución.

Estoy escribiendo la raíz o las operaciones comunes en el modelo (para tener la misma interfaz) y las demás en el controlador del modelo. Si necesito una operación de otro modelo, importo su controlador.

Este enfoque es suficiente para mí y la complejidad de mis aplicaciones.

La respuesta de Hedde es un ejemplo que muestra la flexibilidad de django y python.

Muy interesante pregunta de todos modos!


99
¿Cómo es que decir que es lo suficientemente bueno para usted es útil para mi comprensión de la pregunta?
Chris Wesseling

1
Django tiene mucho más que ofrecer aparte de los modelos django.db., pero la mayor parte del ecosistema depende en gran medida de su modelo que use los modelos django.
andho

1
El patrón de diseño utilizado para desarrollar software. ¡Y django está diseñado para ser utilizado fácilmente para entregar software a mediana o gran escala, no solo páginas web!
Mohammad Torkashvand
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.