📚 Unidad ISS-09 · Catálogo con ModelViewSet — capa 🧠 APRENDER
🛠 Construir este bloque → · ✅ Condiciones de cierre (GATE) · 📝 Evaluación
Capa Página Para qué 🧠 Aprender esta página comprender, explicar y relacionar 🛠 Construir Catálogo con ModelViewSet ejecutar, programar y verificar ✅ GATE Cierre de la unidad condición para pasar al bloque siguiente
Mapa de correspondencias. Cada fila enlaza el mismo tema en las dos capas; los enlaces apuntan a secciones reales del material.
| Tema | 🧠 Aprender (esta página) | 🛠 Construir (ISS técnico) |
|---|---|---|
| Objetivo | A. Objetivos y Requisitos de ISS-09 | Objetivo |
| Recorrido | C. Estructura de Archivos y Paquetes | Construcción |
| Cierre | I. Criterios de Aceptación (AC) | Criterios de aceptación · GATE |
| Evaluación | K. Cuestionario de Preguntas de Defensa Oral Técnica | GATE |
🖥 Presentación del ISS
Presentación de la unidad. Diapositivas de ISS-09 — Catálogo con ModelViewSet (9 diapositivas). Se visualiza aquí, dentro del sitio.
🎬 Video explicativo
Recorrido audiovisual de la unidad. El video recorre el ISS técnico de ISS-09 — Catálogo con ModelViewSet, bloque por bloque.
6:43 min · narración en español · subtítulos activables desde el reproductor.
Infografía
Mapa conceptual
- Abstracción de Vistas
- CatalogViewSet como base ModelViewSet
- Permisos AllowAny en Fase I
- Convertidor de clave lookup_value_converter = 'int'
- Filtrado por estado con query_params ?status=
- Baja lógica en perform_destroy a RecordStatus.INACTIVE
- ViewSets Concretos
- ProductTypeViewSet sobre ProductType.objects.all()
- ProductViewSet sobre Product con select_related
- Optimización contra consultas N+1 en relaciones
- Enrutamiento y URLs
- SimpleRouter con use_regex_path=False
- Registro de product-types y products
- Inclusión global en config/urls.py
- Reglas y Borde HTTP
- Formato JSON camelCase con productTypeId y productTypeName
- Acceso OPEN en Fase I sin token
- Protección de borrado con RestrictedError en base de datos
- Verificación y Pruebas
- ProductApiTests como APITestCase en acceso OPEN
- setUp para crear tipo 'Bebida' en el ORM
- test_product_type_crud del tipo de producto
- test_product_retrieve_update_and_soft_delete de productos
- test_default_status_and_type_restriction para defaults y Restrict
- test_duplicate_product_name para rechazar duplicados
- Criterios y Gate
- AC-09-01: POST de producto activo responde 201
- AC-09-02: Nombre duplicado responde 400
- AC-09-03: Defaults de estado inactive y stock 0
- EVI-09-01: Tests PASS en los tres motores ejecutados
Guía de Estudio: ISS-09 — Catálogo con ModelViewSet en DRF
Esta guía de estudio detalla la arquitectura, implementación, pruebas y criterios de verificación para la unidad ISS-09: Catálogo con ModelViewSet, en la cual se exponen las interfaces de programación (endpoints) de operaciones CRUD (Crear, Leer, Actualizar, Eliminar) para los recursos de catálogo ProductType (Tipos de Producto) y Product (Productos) haciendo uso de las abstracciones avanzadas de Django REST Framework (DRF).
A. Objetivos y Requisitos de ISS-09
Objetivos Principales
- Publicar los endpoints RESTful estandarizados para las entidades
ProductTypeyProductaplicando el patrón homogéneo de consulta y manipulación de datos a través deModelViewSet. - Reutilizar lógica transversal (filtrado dinámico por estado y baja lógica mediante soft delete) extrayendo una vista base del mismo dominio denominada
CatalogViewSet. - Garantizar la eficiencia de las consultas ORM eliminando el problema N+1 mediante
select_related("product_type"). - Mantener el acceso en nivel de visibilidad OPEN (
permission_classes = [AllowAny]) correspondiente a la Fase I del proyecto, sin requerir autenticación ni tokens JWT en esta etapa.
Requisitos Previos
- Habiendo superado la unidad ISS-08 (Client con ModelViewSet).
- Contar con las apps de negocio e infraestructura básica registradas (
apps.product). - Disponer de los modelos
ProductTypeyProductcorrectamente declarados y migrados en la base de datos (con sus restricciones comoMinLengthValidator,MinValueValidator,CheckConstraintyon_delete=models.RESTRICT). - Tener configurados los serializadores
ProductTypeSerializeryProductSerializercon soporte para transformación de nombres en formato camelCase en el borde HTTP (productTypeId,productTypeName).
B. Conceptos Esenciales y Vocabulario Técnico
To enhance comprehension, below is a taxonomy of the technical terms and architectural components applied in ISS-09:
| Concepto Técnico | Definición y Función en el Proyecto |
|---|---|
CatalogViewSet |
Clase base abstracta derivada de viewsets.ModelViewSet que centraliza el comportamiento común de las entidades del catálogo (filtrado dinámico por query param ?status= y baja lógica en perform_destroy). |
ModelViewSet |
Clase de vista de DRF que agrupa automáticamente las implementaciones para las acciones estándar del ciclo de vida REST (list, create, retrieve, update, partial_update, destroy). |
select_related |
Método del ORM de Django para realizar un JOIN SQL explícito en consultas de relaciones 1 → N (Foreign Key), evitando ejecutar N consultas adicionales a la base de datos al acceder al modelo relacionado. |
SimpleRouter |
Enrutador de DRF que mapea automáticamente los métodos HTTP de las peticiones salientes hacia las acciones internas de un ViewSet, generando rutas para colecciones y elementos individuales. |
basename |
Parámetro obligatorio al registrar un ViewSet en el enrutador cuando no se especifica explícitamente queryset en la vista base o cuando se requiere forzar el prefijo de nomenclatura de URLs (ej. product-type-list, product-detail). |
lookup_value_converter |
Atributo de clase en el ViewSet (fijado en "int") que instruye al router para que interprete la variable identificadora de ruta como un entero (<int:pk>) en lugar de expresiones regulares o cadenas de texto. |
perform_destroy |
Método del ciclo de vida de ModelViewSet interceptado para sustituir la eliminación física (DELETE SQL) por una baja lógica, fijando status = RecordStatus.INACTIVE. |
get_queryset |
Método sobrescrito dinámicamente en la vista para interceptar los parámetros de la petición HTTP (request.query_params) y aplicar filtros sobre los datos antes de entregarlos al serializador. |
RestrictedError |
Excepción lanzada por el ORM de Django al intentar eliminar un registro de tipo de producto (ProductType) que posee registros asociados de productos (Product), debido a la restricción on_delete=models.RESTRICT. |
AllowAny |
Clase de permiso de DRF que otorga acceso irrestricto y público (nivel OPEN) a los endpoints expuestos durante la Fase I. |
APITestCase |
Clase base de pruebas proporcionada por DRF que extiende TestCase de Django e integra un cliente HTTP simétrico (self.client) para ejecutar pruebas de integración sobre los endpoints de la API. |
C. Estructura de Archivos y Paquetes
La implementación del módulo de catálogo implica la reescritura de las vistas y pruebas de la app de producto, la creación de la capa de enrutamiento interna y la inclusión de esta en el URLconf principal del proyecto.
Modificaciones en el Árbol del Proyecto
storelab/
├── apps/
│ └── product/
│ ├── views.py # [REESCRITURA] ModiViewSet base CatalogViewSet, ProductTypeViewSet y ProductViewSet
│ ├── urls.py # [CREACIÓN] Configuración de SimpleRouter y registro de rutas del catálogo
│ └── tests.py # [REESCRITURA] Casos de prueba HTTP en nivel OPEN para ProductType y Product
└── config/
└── urls.py # [PARCHE] Inclusión de las rutas de apps/product/urls.py bajo el prefijo "api/"
D. Explicación por Bloques Semánticos de apps/product/views.py
El archivo apps/product/views.py define la lógica de interacción HTTP para las entidades de catálogo. A continuación, se detalla el código fuente completo y la explicación de cada uno de sus bloques semánticos.
from rest_framework import viewsets
from rest_framework.permissions import AllowAny
from apps.common.status import RecordStatus
from apps.product.models import Product, ProductType
from apps.product.serializers import ProductSerializer, ProductTypeSerializer
class CatalogViewSet(viewsets.ModelViewSet):
permission_classes = [AllowAny]
lookup_value_converter = "int"
def get_queryset(self):
queryset = super().get_queryset()
status_value = self.request.query_params.get("status")
if status_value:
queryset = queryset.filter(status=status_value)
return queryset
def perform_destroy(self, instance):
instance.status = RecordStatus.INACTIVE
instance.save(update_fields=["status", "updated_at"])
class ProductTypeViewSet(CatalogViewSet):
queryset = ProductType.objects.all()
serializer_class = ProductTypeSerializer
class ProductViewSet(CatalogViewSet):
queryset = Product.objects.select_related("product_type")
serializer_class = ProductSerializer
Análises por Bloques Semánticos
- Importación de Dependencias e Infraestructura Base:
from rest_framework import viewsets: Importa el paquete de conjuntos de vistas de DRF.from rest_framework.permissions import AllowAny: Otorga acceso abierto en Fase I.from apps.common.status import RecordStatus: Proporciona la enumeración de estados de registros (ACTIVE,INACTIVE).-
Importación de modelos (
Product,ProductType) y sus correspondientes serializadores (ProductSerializer,ProductTypeSerializer). -
Abstracción Jerárquica:
CatalogViewSet: - Declaración: Hereda de
viewsets.ModelViewSet, lo que le permite actuar como clase padre de los recursos de catálogo del mismo dominio. - Atributos de Configuración:
permission_classes = [AllowAny]asegura que en esta etapa no se exija autenticación JWT.lookup_value_converter = "int"fuerza la coincidencia de identificadores de ruta como números enteros (<int:pk>). - Método
get_queryset: Llama asuper().get_queryset()para obtener el conjunto base. Evalúa la presencia del parámetro de consulta?status=. Si está presente en la URI, aplica.filter(status=status_value)para realizar filtrado dinámico. -
Método
perform_destroy: Intercepta la acción de borrado (DELETE). En lugar de ejecutar la sentenciaDELETE FROM, asignainstance.status = RecordStatus.INACTIVEy persiste el cambio especificandoupdate_fields=["status", "updated_at"], ejecutando una baja lógica (soft delete) limpia y eficiente. -
Especialización para Tipos de Producto (
ProductTypeViewSet): - Hereda de
CatalogViewSet. - Define
queryset = ProductType.objects.all(). -
Asigna
serializer_class = ProductTypeSerializer. -
Especialización para Productos (
ProductViewSet): - Hereda de
CatalogViewSet. - Define
queryset = Product.objects.select_related("product_type"). El uso deselect_relatedrealiza una consulta SQL optimizada conINNER JOIN(oLEFT OUTER JOIN), trayendo los datos del tipo de producto asociado en una sola interacción con la base de datos. - Asigna
serializer_class = ProductSerializer.
E. Explicación por Bloques Semánticos de Routing (apps/product/urls.py y config/urls.py)
La capa de enrutamiento se encarga de convertir las peticiones HTTP entrantes en invocaciones específicas a los métodos del ViewSet.
Código de apps/product/urls.py
from rest_framework.routers import SimpleRouter
from apps.product.views import ProductTypeViewSet, ProductViewSet
router = SimpleRouter(use_regex_path=False)
router.register("product-types", ProductTypeViewSet, basename="product-type")
router.register("products", ProductViewSet, basename="product")
urlpatterns = router.urls
Explicación Semántica del Enrutamiento Local (apps/product/urls.py)
- Instanciación de
SimpleRouter:router = SimpleRouter(use_regex_path=False)configura el enrutador para emplear los convertidores de ruta nativos de Django en lugar de expresiones regulares complejas. - Registro de Recursos (
register): router.register("product-types", ProductTypeViewSet, basename="product-type"): Publica la colección bajo el segmentoproduct-types/y los detalles bajoproduct-types/<int:pk>/.router.register("products", ProductViewSet, basename="product"): Publica la colección bajoproducts/y los detalles bajoproducts/<int:pk>/.- Nombres de Ruta Generados Automáticamente:
Para
products:product-list(GET/POST /products/) yproduct-detail(GET/PUT/PATCH/DELETE /products/<int:pk>/). - Exportación: Se asigna
urlpatterns = router.urls.
Parche en el URLconf Raíz (config/urls.py)
Se añade la inclusión del archivo de rutas de la app product bajo la raíz de la API:
# Dentro de config/urls.py (urlpatterns)
path("api/", include("apps.client.urls")),
path("api/", include("apps.product.urls")), # Parche del ISS-09
Django evalúa los patrones en orden y concatena los prefijos: api/ + products/ = /api/products/.
Aclaración Explícita sobre el Endpoint
/api/products/available/:
En el alcance del ISS-09, el endpoint especializado para consultar únicamente productos disponibles (/api/products/available/) NO existe. Su diseño responde a una lectura filtrada particular basada enListAPIView(GenericAPIView) y su inclusión formal está diferida hasta la unidad ISS-10. En este ISS-09 únicamente están publicadas las rutas estándar del CRUD generadas por elSimpleRouter.
F. Pruebas de API HTTP en Acceso OPEN (apps/product/tests.py)
Las pruebas del módulo se ejecutan en nivel de acceso OPEN (sin autenticación, ni encabezados Authorization: Bearer, ni asignación de permisos RBAC). El método setUp se limita a inicializar datos de apoyo directamente en la base de datos mediante el ORM.
Código Completo de apps/product/tests.py
from decimal import Decimal
from django.db.models.deletion import RestrictedError
from rest_framework.test import APITestCase
from apps.common.status import RecordStatus
from apps.product.models import Product, ProductType
class ProductApiTests(APITestCase):
def setUp(self):
self.product_type = ProductType.objects.create(name="Bebida", status=RecordStatus.ACTIVE)
def test_product_type_crud(self):
created = self.client.post(
"/api/product-types/",
{"name": "Lácteo", "description": "Fríos"},
format="json",
)
self.assertEqual(created.status_code, 201)
self.assertEqual(created.data["status"], RecordStatus.INACTIVE)
pk = created.data["id"]
self.assertEqual(self.client.get("/api/product-types/").status_code, 200)
detail = self.client.get(f"/api/product-types/{pk}/")
self.assertEqual(detail.data["name"], "Lácteo")
updated = self.client.patch(
f"/api/product-types/{pk}/",
{"status": "active"},
format="json",
)
self.assertEqual(updated.status_code, 200)
deleted = self.client.delete(f"/api/product-types/{pk}/")
self.assertEqual(deleted.status_code, 204)
self.assertEqual(ProductType.objects.get(pk=pk).status, RecordStatus.INACTIVE)
def test_product_retrieve_update_and_soft_delete(self):
created = self.client.post(
"/api/products/",
{
"productTypeId": self.product_type.id,
"name": "Leche",
"price": "3.20",
"stock": 2,
"status": "active",
},
format="json",
)
pk = created.data["id"]
detail = self.client.get(f"/api/products/{pk}/")
self.assertEqual(detail.status_code, 200)
updated = self.client.patch(f"/api/products/{pk}/", {"stock": 9}, format="json")
self.assertEqual(updated.status_code, 200)
self.assertEqual(updated.data["stock"], 9)
deleted = self.client.delete(f"/api/products/{pk}/")
self.assertEqual(deleted.status_code, 204)
self.assertEqual(Product.objects.get(pk=pk).status, RecordStatus.INACTIVE)
def test_default_status_and_type_restriction(self):
created = self.client.post(
"/api/products/",
{"productTypeId": self.product_type.id, "name": "Té", "price": "1.00"},
format="json",
)
self.assertEqual(created.status_code, 201)
self.assertEqual(created.data["status"], "inactive")
self.assertEqual(created.data["stock"], 0)
with self.assertRaises(RestrictedError):
self.product_type.delete()
def test_duplicate_product_name(self):
payload = {"productTypeId": self.product_type.id, "name": "Café", "price": "3.00"}
self.assertEqual(self.client.post("/api/products/", payload, format="json").status_code, 201)
self.assertEqual(self.client.post("/api/products/", payload, format="json").status_code, 400)
self.assertEqual(Product.objects.get(name="Café").price, Decimal("3.00"))
Análises Detallado de los Métodos de Prueba
setUp(self): Crea una instancia deProductTypecon el nombre"Bebida"y estadoRecordStatus.ACTIVEmediante el ORM. Esto provee la llave primaria requerida para probar la creación de productos sin pasar por autenticación HTTP.test_product_type_crud(self):- Ejecuta
POST /api/product-types/para crear"Lácteo". Verifica código HTTP 201 y valor por defecto de estadoinactive. - Realiza
GET /api/product-types/yGET /api/product-types/{pk}/comprobando código 200 y coincidencia de atributos. - Envía
PATCH /api/product-types/{pk}/actualizando el estado a"active". - Ejecuta
DELETE /api/product-types/{pk}/. Comprueba respuesta 204 y verifica mediante el ORM que el registro continúa en la base de datos constatus == RecordStatus.INACTIVE. test_product_retrieve_update_and_soft_delete(self):- Envía la carga JSON en formato camelCase conteniendo
"productTypeId": self.product_type.id. - Consulta el detalle (
GET), aplica una actualización parcial (PATCH) cambiando la propiedad"stock"a9, e invoca la baja lógica (DELETE). - Valida la persistencia de los cambios y la retención física de la fila en estado inactivo.
test_default_status_and_type_restriction(self):- Realiza un
POSTenviando únicamente los campos obligatorios (productTypeId,name,price). - Valida que los valores por defecto autogenerados sean
status = "inactive"ystock = 0. - Intenta eliminar el tipo de producto Padre directamente desde el ORM (
self.product_type.delete()). Comprueba de forma afirmativa que se lanza la excepciónRestrictedErrorprovocada por la clave foránea configurada conon_delete=models.RESTRICT. test_duplicate_product_name(self):- Intenta registrar dos productos con el mismo nombre (
"Café"). - La primera petición responde 201; la segunda es rechazada con un código HTTP 400 (Bad Request) debido a la restricción
unique=Trueen la columnaname.
G. Análisis Comparativo y de Arquitectura
PETICIÓN HTTP (JSON)
│
▼
[ config/urls.py (api/) ]
│
▼
[ apps/product/urls.py (SimpleRouter) ]
│
┌───────────────┴───────────────┐
▼ ▼
ProductTypeViewSet ProductViewSet
(CatalogViewSet) (CatalogViewSet)
│ │
│ ▼
│ select_related("product_type")
│ │
└───────────────┬───────────────┘
▼
[ CatalogViewSet ]
├── query_params ?status=
└── perform_destroy (Soft Delete)
│
▼
Base de Datos SQL
Ventajas de la Abstracción con CatalogViewSet
- Eliminación de Redundancia (DRY - Don't Repeat Yourself): Evita la duplicación del método
perform_destroyy de la lógica de filtrado por estado (get_queryset) en múltiples clases del mismo dominio. - Mantenibilidad Estructurada: Si la regla de negocio para la baja lógica o el filtrado de auditoría se modifica a futuro, el cambio se realiza en un único punto del código (
CatalogViewSet).
Beneficios de select_related("product_type") contra el Problema N+1
- Problema N+1 SQL: Al listar N productos que deben mostrar el nombre de su tipo de producto (
product_type_name), un ORM no optimizado realizaría 1 consulta para obtener los N productos y N consultas adicionales a la tablaproduct_typespara resolver cada nombre (Total: 1 + N consultas). - Solución de Django: Al configurar
queryset = Product.objects.select_related("product_type"), el ORM ejecuta una sola consulta SQL utilizando un operadorJOIN: Esto reduce dramáticamente el tiempo de I/O de red y la latencia en el servidor de base de datos.
Protección de Integridad Referencial (on_delete=models.RESTRICT)
- Diferencia frente a
CASCADE:CASCADEeliminaría en cascada todos los productos pertenecientes a un tipo de producto borrado, destruyendo información histórica y contable. - Comportamiento con
RESTRICT: Impide la eliminación del registro Padre (ProductType) si existen registros Hijos (Product) vinculados a él. El intento de borrado es interceptado por el ORM y lanza una excepción de tipoRestrictedError, preservando de esta forma la coherencia e integridad relacional del catálogo.
H. Errores Frecuentes y Buenas Prácticas
┌─────────────────────────────────────────┬──────────────────────────────────────────┐
│ ERROR FRECUENTE │ IMPACTO / CONSECUENCIA │
├─────────────────────────────────────────┼──────────────────────────────────────────┤
│ 1. Duplicar código entre ViewSets │ Violación de DRY. Dificulta mantener la │
│ del mismo dominio de catálogo. │ baja lógica o el filtrado centralizado. │
├─────────────────────────────────────────┼──────────────────────────────────────────┤
│ 2. Omitir `select_related` en │ Degeneración del rendimiento de la BD │
│ `ProductViewSet.queryset`. │ por la ejecución de N+1 consultas SQL. │
├─────────────────────────────────────────┼──────────────────────────────────────────┤
│ 3. Intentar eliminar tipos de producto │ Provoca un fallo no controlado si no se │
│ con hijos ignorando `RestrictedError`│ maneja la restricción de integridad. │
├─────────────────────────────────────────┼──────────────────────────────────────────┤
│ 4. Incluir la vista de productos │ Violación de los límites de diseño del │
│ disponibles antes del ISS-10. │ ISS-09 (extensión innecesaria). │
└─────────────────────────────────────────┴──────────────────────────────────────────┘
- Duplicar código entre ViewSets del mismo dominio:
- Incorrección: Implementar manualmente
perform_destroyyget_querysettanto enProductTypeViewSetcomo enProductViewSet. - Solución: Heredar ambas clases de
CatalogViewSet. - Omitir
select_relateden el QuerySet de Productos: - Incorrección: Declarar
queryset = Product.objects.all(). - Solución: Declarar
queryset = Product.objects.select_related("product_type")para garantizar la carga impaciente (eager loading) de la clave foránea. - Intentar eliminar un
ProductTypecon productos asociados esperando una eliminación física: - Incorrección: Ignorar que los productos activos o inactivos mantienen la integridad relacional vía
models.RESTRICT. - Solución: Capturar o prever la excepción
RestrictedErrory aplicar borrado lógico sobre los registros en lugar del borrado físico. - Intentar registrar la vista
AvailableProductListAPIViewen este ISS: - Incorrección: Introducir componentes del ISS-10 de forma prematura.
- Solución: Mantener únicamente los dos
ModelViewSetenapps/product/views.pyy sus dos registros enSimpleRouter.
I. Criterios de Aceptación (AC)
- AC-09-01: Crear un producto activo con stock hace que aparezca de manera inmediata en la lista del catálogo.
- AC-09-02: Intentar registrar un producto con un nombre ya existente (duplicado) responde con un código de estado HTTP 400 Bad Request.
- AC-09-03: Al crear un producto omitiendo las propiedades
statusystock, el sistema asigna de forma predeterminada los valoresstatus = "inactive"ystock = 0.
J. Verificación, Evidencias y Tabla GATE
Comandos de Verificación
Para validar el cumplimiento de la unidad, se ejecutan las pruebas automatizadas de la app product:
Evidencias de Ejecución
- EVI-09-01: Ejecución exitosa de la suite de pruebas de productos (
ProductApiTests) retornandoPASS(código 0) en los motores de base de datos evaluados.
Tabla GATE (Criterio de Salida)
| Criterio de Aceptación (AC) | Método de Verificación | Evidencia Documentada | Resultado |
|---|---|---|---|
| AC-09-01 | ProductApiTests.test_product_retrieve_update_and_soft_delete |
EVI-09-01 | PASS |
| AC-09-02 | ProductApiTests.test_duplicate_product_name |
EVI-09-01 | PASS |
| AC-09-03 | ProductApiTests.test_default_status_and_type_restriction |
EVI-09-01 | PASS |
K. Cuestionario de Preguntas de Defensa Oral Técnica
A continuación, se presentan preguntas técnicas de nivel avanzado con sus respectivas respuestas estructuradas y justificaciones pedagógicas, diseñadas para la evaluación y defensa oral del trabajo realizado en el ISS-09:
Pregunta 1: ¿Por qué se creó la clase CatalogViewSet en lugar de hacer que ProductTypeViewSet y ProductViewSet heredaran directamente de viewsets.ModelViewSet?
- Respuesta Técnica: Se creó
CatalogViewSetcomo una clase base de abstracción para evitar la duplicación de código (principio DRY). Ambas entidades del dominio del catálogo comparten exactamente el mismo comportamiento para la baja lógica (modificar el estado aINACTIVEmedianteperform_destroy) y para el filtrado dinámico mediante parámetros de consulta en la URL (get_querysetleyendo?status=). - Justificación Pedagógica: Promueve el entendimiento de la jerarquía de clases en Python y DRF. Demuestra que la programación orientada a objetos permite encapsular reglas de negocio comunes en clases abstractas intermedias, facilitando el mantenimiento y garantizando la consistencia del comportamiento en todo el módulo.
Pregunta 2: ¿Qué problema de rendimiento técnico resuelve el uso de Product.objects.select_related("product_type") en la propiedad queryset de ProductViewSet?
- Respuesta Técnica: Resuelve el problema de las N+1 consultas SQL. Al serializar una lista de N productos,
ProductSerializerdebe resolver la propiedad de solo lecturaproduct_type_name(extraída deproduct_type.name). Sinselect_related, Django ejecutaría 1 consulta inicial para obtener los productos y N consultas adicionales para obtener el nombre del tipo de cada producto. Conselect_related, Django realiza unJOINa nivel de base de datos en la primera consulta, reduciendo el total de peticiones SQL a exactamente 1. - Justificación Pedagógica: Obliga al estudiante a analizar la eficiencia computacional de la capa ORM. Permite visibilizar que una abstracción de alto nivel como DRF puede ocultar ineficiencias severas en el acceso a la base de datos si no se comprende cómo el ORM traduce las operaciones a SQL.
Pregunta 3: En el método perform_destroy de CatalogViewSet, ¿por qué se utiliza instance.save(update_fields=["status", "updated_at"]) en lugar de una llamada simple a instance.save()?
- Respuesta Técnica: El uso explícito del argumento
update_fieldsoptimiza la sentencia SQL generada (UPDATE), limitando la actualización únicamente a las columnasstatusyupdated_at. Esto previene la sobreescritura accidental de otras columnas en entornos de concurrencia y reduce la carga sobre la base de datos al evitar enviar la totalidad de los campos del registro. - Justificación Pedagógica: Fomenta las buenas prácticas en la persistencia de datos con el ORM de Django, enseñando a los estudiantes a controlar de manera quirúrgica las sentencias de actualización enviadas al motor SQL.
Pregunta 4: ¿Por qué al invocar delete() sobre un ProductType que posee productos asociados se eleva una excepción RestrictedError en lugar de eliminarse los productos relacionados?
- Respuesta Técnica: En el modelo
Product, el campoproduct_typefue definido con la regla de integridad referencialon_delete=models.RESTRICT. Esto indica explícitamente al ORM de Django que debe bloquear cualquier intento de eliminación del registro Padre (ProductType) mientras exista al menos un registro Hijo (Product) apuntando a su clave primaria, protegiendo así la integridad referencial del sistema. - Justificación Pedagógica: Evalúa la comprensión sobre el diseño de bases de datos relacionales y el modelado conceptual. Asegura que el estudiante distinga entre la eliminación descontrolada en cascada (
CASCADE) y la preservación estricta de la integridad relacional mediante restricciones de borrado (RESTRICT).
Pregunta 5: ¿Por qué en la clase de prueba ProductApiTests se utiliza format="json" en las llamadas del cliente HTTP (self.client.post(...)) y cómo se relaciona esto con los nombres de campos como productTypeId?
- Respuesta Técnica: Especificar
format="json"simula de manera precisa una petición cliente real enviando una carga útil en formato JSON. Al procesar la solicitud, el middlewaredjangorestframework-camel-caseintercepta el cuerpo en camelCase (productTypeId), convirtiéndolo dinámicamente al nombre de campo Python/Django en snake_case (product_type_idoproduct_type) definido en el serializador, manteniendo la coherencia con el contrato de la API. - Justificación Pedagógica: Demuestra la integración entre la suite de pruebas, la capa de serialización y los parsers/renderers de DRF. El estudiante debe entender que el contrato HTTP debe hablar el estándar JSON (camelCase) independientemente de las convenciones internas de nomenclatura del lenguaje del servidor (snake_case).
Pregunta 6: ¿Por qué el endpoint /api/products/available/ no forma parte de las rutas publicadas en el SimpleRouter de ISS-09?
- Respuesta Técnica: Un
SimpleRoutermapea únicamente las acciones CRUD estándar de unViewSet(list,create,retrieve,update,partial_update,destroy). El endpoint/api/products/available/representa una consulta especializada que responde a una regla de filtrado específica que no encaja sintácticamente con un CRUD convencional sobre la colección. Por ello, se implementará formalmente mediante una vista especializadaListAPIView(GenericAPIView) en la unidad ISS-10. - Justificación Pedagógica: Ayuda a diferenciar los patrones de arquitectura de APIs RESTful: la conveniencia de los
ViewSetpara recursos homogéneos estándar frente al uso de vistas genéricas (GenericAPIView/ListAPIView) para consultas filtradas o endpoints especializados.
Navegación de la ruta: ← ISS-08 · 🛠 Construir · ↑ Ruta Django · → ISS-09 · 🛠 Construir