python django model-view-controller data-access-layer business-logic-layer

python - Separación de lógica de negocio y acceso a datos en django.



model-view-controller data-access-layer (8)

Django emplea un tipo ligeramente modificado de MVC. 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 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 base de datos. En algunos aspectos, comparte el deber con la "vista" de Django como 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 todavía es un código modelo. De hecho, no es necesario que los modelos interactúen con la base de datos, por lo que es posible que tenga modelos que existan completamente como una capa interactiva para una API externa. Es un concepto mucho más libre de un "modelo".

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 un 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 solo era responsable del trabajo con datos de una base de datos) también está enviando correos electrónicos, pasando el 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 hace difícil controlar. Por ejemplo, en mi aplicación hay al menos tres formas de crear nuevas instancias de User , pero técnicamente debería crearlas de manera uniforme.
  3. No siempre me doy cuenta cuando los métodos y las propiedades de mis modelos se vuelven no deterministas y cuando desarrollan efectos secundarios.

Aquí hay un ejemplo simple. Al principio, el modelo de User 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 este enfoque que se puede aplicar en Django?


Django está diseñado para ser utilizado fácilmente para entregar páginas web. Si no se siente cómodo con esto, tal vez 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 otras en el controlador del modelo. Si necesito una operación de otro modelo importo su controlador.

Este enfoque es suficiente para mí y para 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!


En Django, la estructura MVC es como dijo Chris Pratt, diferente del modelo MVC clásico usado en otros marcos, creo que la razón principal para hacer esto 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 deben usarse solo para gestionar solicitudes HTTP, se llaman y responden a ellas. Las vistas se comunican con el resto de su aplicación (formularios, modelos, clases personalizadas, o en casos simples directamente con modelos). Para crear la interfaz utilizamos plantillas. Las plantillas son similares a una cadena de Django, asignan un contexto en ellas y este contexto fue comunicado a la vista por la aplicación (cuando la vista lo solicita).

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 debas crear grandes archivos de models.py (de hecho, un muy buen consejo es dividir tus modelos en diferentes archivos, colocarlos en una carpeta llamada ''modelos'', crear un archivo ''__init__.py'' en este carpeta donde importa todos sus modelos y finalmente utiliza el atributo ''app_label'' de la clase models.Model). El modelo debería abstraerlo de operar con datos, hará que su aplicación sea más sencilla. También debe, si es necesario, crear clases externas, como "herramientas" para sus modelos. También puede utilizar 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 con los datos, en algunos casos de programas pequeños, el uso de vistas para consultar o insertar datos sería suficiente. Los casos más comunes utilizarán Forms o ModelForms, que en realidad son "controladores". Esto no es otra cosa que una solución práctica a un problema común, y una muy rápida. Es lo que un sitio web utiliza para hacer.

Si los formularios no son suficientes para usted, entonces debe crear sus propias clases para hacer la magia, un buen ejemplo de esto es la aplicación de administración: puede leer el código ModelAmin, que en realidad 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, torcido 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 hacer un código de buena calidad, respetar o no la filosofía de MVC, hacerlo basado en módulos y crear sus propias capas de abstracción. Es muy flexible.

Mi consejo: lea todo el código que 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, esto es una ciencia imprecisa, django solo le proporciona buenas herramientas que puede usar para aliviar algunos problemas (como la interfaz de administración, la validación de formularios web, i18n, la implementación de patrones de observadores, todo lo mencionado anteriormente y otros), pero los buenos diseños provienen de diseñadores experimentados.

PS .: use la clase ''User'' de la aplicación auth (del django estándar), puede crear, por ejemplo, perfiles de usuario, o al menos leer su código, será útil para su caso.


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

Uno puede definir las clases QuerySet para los modelos para hacer consultas de filtro y hacer clic en. Después de eso, puede usar esta clase de conjunto de consultas para el administrador del modelo, como hacen las clases Administrador integrado y QuerySet.

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


En primer lugar, no te repitas .

Entonces, por favor tenga cuidado de no trabajar en exceso, a veces es solo una pérdida de tiempo y hace que alguien pierda el enfoque en lo que es importante. Revisa el zen de python de vez en cuando.

Echa un vistazo a los proyectos activos.

  • más personas = más necesidad de organizar adecuadamente
  • El repositorio de django tienen una estructura sencilla.
  • El repositorio pip tiene una estructura de directorios directa.
  • El repositorio de tela también es bueno para mirar.

    • puede colocar todos sus modelos en yourapp/models/logicalgroup.py
  • por ejemplo, el User , el Group y los modelos relacionados pueden ir bajo yourapp/models/users.py
  • Por ejemplo, Poll , Question , Answer ... podría ir bajo yourapp/models/polls.py
  • carga lo que necesitas en __all__ dentro de yourapp/models/__init__.py

Más sobre MVC

  • modelo es su información
    • esto incluye tus datos reales
    • esto también incluye su sesión / cookie / cache / fs / index data
  • 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 ajustar con request.GET / request.POST ... etc
    • Piense en la paginación o el filtrado también.
  • los datos actualizan la vista
    • Las plantillas toman los datos y los formatean en consecuencia.
    • API incluso sin plantillas son parte de la vista; por ejemplo, tastypie o piston
    • Esto también debe tener en cuenta el middleware.

Aprovecha el middleware / templatetags

  • Si necesita algo de trabajo por hacer para cada solicitud, el middleware es un camino a seguir.
    • por ejemplo, añadiendo marcas de tiempo
    • Por ejemplo, actualizando las métricas sobre los resultados de la página.
    • por ejemplo, llenar un caché
  • Si tiene fragmentos de código que siempre vuelven a aparecer para formatear objetos, las etiquetas de plantilla son buenas.
    • por ejemplo, pestañas activas / url breadcrumbs

Aproveche los manager

  • la creación del User puede ir en un UserManager(models.Manager) .
  • Los detalles sangrientos de las instancias deberían ir en los models.Model . models.Model .
  • Los detalles sangrientos para queryset pueden ir en un models.Manager .
  • es posible que desee crear un User uno en uno, por lo que puede pensar que debería vivir en el propio modelo, pero al crear el objeto, es probable que 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()

Hacer uso de formularios cuando sea posible.

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

Utilice los comandos de gestión cuando sea posible

  • por ejemplo, su yourapp/management/commands/createsuperuser.py
  • por ejemplo, su yourapp/management/commands/activateinbulk.py

Si tienes lógica de negocios, puedes separarla.

  • django.contrib.auth usa backends , al igual que db tiene un backend ... etc.
  • agregue una setting para su lógica empresarial (por ejemplo, AUTHENTICATION_BACKENDS )
  • puedes usar django.contrib.auth.backends.RemoteUserBackend
  • puede usar yourapp.backends.remote_api.RemoteUserBackend
  • usted podría usar yourapp.backends.memcached.RemoteUserBackend
  • delegar la lógica de negocio 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 sobre los límites de la interfaz

  • ¿El código que desea utilizar es realmente parte de los modelos? -> yourapp.models
  • ¿Es el código parte de la lógica de negocios? -> yourapp.vendor
  • ¿El código es parte de las herramientas genéricas / libs? -> yourapp.libs
  • ¿Es el código parte de las librerías de lógica de negocios? -> yourapp.libs.vendor o yourapp.vendor.libs
  • Aquí hay una buena: ¿puedes probar tu código de forma independiente?
    • si bien :)
    • no, puedes tener un problema de interfaz
    • cuando hay una separación clara, unittest debería ser una brisa con el uso de burla
  • ¿Es la separación lógica?
    • si bien :)
    • No, puede 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, refactor podría ser mucho trabajo.
    • No, eso es simplemente increíble!

En definitiva, 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 necesitas y los límites te ayudarán.


Parece que está preguntando acerca de la diferencia entre el modelo de datos y el modelo de dominio ; este último es donde puede encontrar la lógica de negocios y las entidades según lo 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, hay algunos patrones y herramientas comunes que se pueden usar 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 .

Pensando en Comandos

Comencemos por observar los comandos en su ejemplo: "activar este usuario" y "desactivar este usuario". Lo bueno de los comandos es que se pueden expresar fácilmente mediante pequeños escenarios de "cuándo y cuándo":

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

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, su registro del sistema, etc.

Dichos escenarios también le ayudan realmente a configurar un entorno de desarrollo guiado 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; ambas 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í se 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?)

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 en Consultas

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

Antes de comenzar a responder estas consultas, 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 hacen 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 con 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 dependemos de una API externa.

Hacer 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, puedes considerar las siguientes opciones.

Etiquetas y filtros personalizados

La primera alternativa es útil para consultas que son meramente 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 meramente de presentación, puede agregar consultas a su services.py (si está usando eso), o introducir un módulo queries.py :

consultas.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 de proxy son muy útiles en el contexto de la lógica empresarial y los informes. Básicamente se define un subconjunto mejorado de su modelo. Puede anular el QuerySet base de un Administrador anulando el método Manager.get_queryset() .

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 las consultas que son intrínsecamente 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 tus comandos. Esto es muy útil si estos modelos solo son cambiados por uno o dos comandos.

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

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 varios modelos de consulta sincronizados con su modelo original. Además, el procesamiento de la señal se puede descargar para tareas en segundo plano, utilizando Celery o estructuras similares.

señales.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)

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

Mantenerlo limpio

Cuando se utiliza este enfoque, resulta ridículamente fácil determinar si su código se mantiene limpio. Solo sigue estas pautas:

  • ¿Mi modelo contiene métodos que hacen más que administrar el estado de la base de datos? Debe extraer un comando.
  • ¿Mi modelo contiene propiedades que no se asignan a los campos de la base de datos? Debe extraer una consulta.
  • ¿Mi modelo de infraestructura de referencia que no es mi base de datos (como el correo)? Debe 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 base de datos? Debe extraer un comando.

Algunas referencias

Documentación Django: modelos proxy.

Documentación Django: Señales.

Arquitectura: Diseño impulsado por dominio


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 de helicóptero de lo que está sucediendo. Heredé esta práctica de un colega mío que usa mucho esta técnica de capas 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 """ if limit: return Book.objects.filter(**filters)[:limit] return Book.objects.filter(**filters)

vistas.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í, normalmente llevo modelos, vistas y servicios a nivel de módulo y aún más, dependiendo del tamaño del proyecto.


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

Como tal, diseñé un truco que me permite separar la lógica de las definiciones del modelo y aún así obtener todas las sugerencias de mi IDE.

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

  • Las definiciones de base de datos siguen siendo solo eso, sin lógica "basura" adjunta
  • La lógica relacionada con el modelo se coloca de forma ordenada 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 mi models.py se llenaba demasiado y tenía que separar la lógica. La separación es suave e iterativa: podría hacer una función en un momento o en toda la clase o en la totalidad de los modelos.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 ...

aplicación / lógica / usuario.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 el modelo de usuario. Pero como esto obviamente es un truco, estoy muy feliz de aceptar la pequeña molestia de especificar siempre el tipo para self parámetro self .