Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado enlaces rotos y formato. En Servidor continúas la implementación del mismo producto.
Se explica
25 minutos · explicación y demostración
Llegas con dos entidades, sus operaciones CRUD y una colección que comprueba un escenario completo. Hoy conectarás las entidades: consultarás los elementos que pertenecen a otro recurso y comprobarás que una persona puede utilizar esa versión desde tu repositorio.
Una relación en memoria
En el ejemplo, cada tarea guarda un proyectoId. Ese número identifica su proyecto: dos tareas con proyectoId igual a 7 pertenecen al mismo proyecto. Todavía no hay una base de datos que compruebe esa relación; el código tendrá que buscar y comparar los identificadores.
La ruta que expone esa relación es GET /proyectos/7/tareas. Las tareas de un proyecto forman una subcolección: un conjunto que no se consulta por sí mismo, sino a través del recurso del que depende. Por eso la ruta empieza por el padre y la consulta requiere dos decisiones, en este orden:
- Buscar el proyecto 7. Si no existe, responder
404. - Si existe, seleccionar sus tareas. Devolver
200con ellas, o con[]si no tiene ninguna.
Una lista vacía describe un proyecto existente sin tareas. Un 404 indica que no se ha encontrado el proyecto solicitado. Esta diferencia debe quedar en las pruebas.
Compartir las listas entre controladores
Hasta ahora cada controlador conserva su propia lista. La nueva consulta necesita leer proyectos y tareas a la vez. Crear otra lista en el controlador de proyectos no serviría: estaría vacía aunque el controlador de tareas ya hubiera guardado registros.
Reuniremos las dos listas en una clase MemoriaProyecto marcada con la anotación @Component. Ese mecanismo no es nuevo, aunque la anotación sí lo sea. Desde la UD1 tus controladores atienden peticiones sin que nadie los construya desde main: @SpringBootApplication activa el escaneo de componentes, Spring recorre el paquete principal y sus subpaquetes, localiza las clases marcadas, crea una instancia de cada una y la conserva mientras la aplicación se ejecuta. @RestController es una especialización de @Component, de modo que tus controladores ya eran clases gestionadas por Spring. Lo único nuevo hoy es que marcas tú una clase propia que no atiende rutas.
Una vez registrada, cada controlador declara que la necesita escribiéndola como parámetro de su constructor. Spring llama a ese constructor al arrancar y le entrega la instancia que ya tiene creada. Esta forma de recibir un objeto necesario en lugar de construirlo se denomina inyección de dependencias, y como la instancia es única, ambos controladores reciben exactamente la misma. En la UD4 estudiarás el contenedor que hace ese trabajo y cómo organizar las responsabilidades entre capas; hoy este mecanismo sirve únicamente para compartir los datos existentes.
De ahí que escribir new MemoriaProyecto() dentro de un controlador anule el propósito del cambio: cada new produce un objeto distinto, con sus dos listas recién creadas y vacías, que es el problema que la clase pretende resolver. La instancia compartida solo llega por el constructor.
Queda un detalle de Java, ajeno a Spring, sobre el que se apoya toda la sesión. Cuando el controlador guarda this.tareas = memoria.getTareas(), no copia la lista: almacena una referencia al mismo objeto ArrayList. Por eso un add ejecutado desde TareaController resulta visible después desde ProyectoController. Si el método devolviera una copia, cada controlador volvería a trabajar sobre datos propios y el código compilaría y respondería igual, ocultando el fallo.
La memoria sigue siendo temporal y se vacía al reiniciar. Tiene además una segunda limitación: al existir una sola instancia compartida por todas las peticiones, y atender Tomcat cada petición en un hilo distinto, dos escrituras simultáneas operan a la vez sobre la misma lista. ArrayList no es thread-safe, por lo que esta versión no garantiza escrituras concurrentes correctas. Resulta suficiente para aprender el contrato HTTP; la persistencia llegará en la UD5.
Se trabaja
140 minutos · implementación guiada sobre el proyecto propio
Adapta Proyecto, Tarea y sus nombres de campos a la relación real de tu producto. Continúa en una rama de trabajo y conserva las operaciones ya comprobadas.
Paso 1 · Comprobar la versión anterior — 10 minutos
- Arranca el backend y ejecuta la colección de la sesión 7.
- Si falla, localiza la primera petición incorrecta y corrígela antes de cambiar el almacenamiento.
- Identifica qué controlador guarda cada lista y qué entidad contiene la referencia a la otra. En el ejemplo será
Tarea.proyectoId.
Paso 2 · Compartir los datos existentes — 25 minutos
- Bajo tu paquete base, junto a
modelycontroller, crea el paquetememoria. - Crea
MemoriaProyecto.javacon este contenido. Sustituyecom.ejemplo.gestorpor el paquete de tu aplicación.
package com.ejemplo.gestor.memoria;
import com.ejemplo.gestor.model.Proyecto;
import com.ejemplo.gestor.model.Tarea;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Component;
@Component
public class MemoriaProyecto {
private final List<Proyecto> proyectos = new ArrayList<>();
private final List<Tarea> tareas = new ArrayList<>();
public List<Proyecto> getProyectos() { return proyectos; }
public List<Tarea> getTareas() { return tareas; }
}
- En
TareaController, sustituye la declaración de su lista por este campo y constructor. ImportaMemoriaProyecto. ConservasiguienteIdy todos los métodos HTTP existentes.
private final List<Tarea> tareas;
public TareaController(MemoriaProyecto memoria) {
this.tareas = memoria.getTareas();
}
- En
ProyectoController, sustituye su lista por estos campos y constructor. Importa tambiénTarea,ListyMemoriaProyectosi faltan.
private final List<Proyecto> proyectos;
private final List<Tarea> tareas;
public ProyectoController(MemoriaProyecto memoria) {
this.proyectos = memoria.getProyectos();
this.tareas = memoria.getTareas();
}
- Si ya había un constructor, incorpora estas asignaciones a ese constructor; no dejes dos formas distintas de construir el controlador. No conserves los antiguos
new ArrayList<>()en esos campos ni escribasnew MemoriaProyecto()en los controladores. - Reinicia y ejecuta la colección. Las rutas y respuestas anteriores deben seguir funcionando.
Errores frecuentes al compartir la memoria
Los dos primeros impiden arrancar y el tercero no, que es lo que lo hace peligroso.
1 · La aplicación no arranca y el registro termina con un bloque APPLICATION FAILED TO START:
Parameter 0 of constructor in com.ejemplo.gestor.controller.TareaController required a bean of type 'com.ejemplo.gestor.memoria.MemoriaProyecto' that could not be found.
Spring llama bean a cada instancia que administra, término que estudiarás en la UD4. El mensaje dice que un controlador pide por constructor algo que nadie ha registrado, y hay dos causas posibles: falta la anotación @Component sobre MemoriaProyecto, o la clase no cuelga del paquete de la clase marcada con @SpringBootApplication, de modo que el escaneo de componentes no la encuentra.
2 · El arranque falla indicando que hay más de un constructor. Un controlador con dos constructores no le permite a Spring decidir cuál usar. Deja uno solo, con todos los parámetros que necesite la clase.
3 · La aplicación arranca, pero la consulta anidada devuelve [] aunque existan tareas de ese proyecto. Es el síntoma de que los datos siguen separados: revisa que ningún controlador conserve un new ArrayList<>() en esos campos ni construya su propia new MemoriaProyecto(). Este fallo no produce ninguna excepción, y solo se detecta comprobando el contenido de la respuesta.
Paso 3 · Consultar una relación — 30 minutos
- Si
Tareano tiene la referencia, añadeprivate int proyectoId;y los métodosgetProyectoId()ysetProyectoId(int proyectoId). Si ya existe, reutilízala. - Permite enviarla en el POST. Si tu código guarda directamente el objeto recibido, Jackson ya la habrá asignado; si copia campos a otro objeto, añade esa copia. Conserva el identificador de la tarea asignado por el servidor.
- En
ProyectoController, que ya tiene@RequestMapping("/proyectos"), añade este método. ImportaArrayListyResponseEntitysi faltan.
@GetMapping("/{id}/tareas")
public ResponseEntity<List<Tarea>> tareasDelProyecto(
@PathVariable(name = "id") int id) {
boolean existe = false;
for (Proyecto proyecto : proyectos) {
if (proyecto.getId() == id) {
existe = true;
break;
}
}
if (!existe) {
return ResponseEntity.notFound().build();
}
List<Tarea> resultado = new ArrayList<>();
for (Tarea tarea : tareas) {
if (tarea.getProyectoId() == id) {
resultado.add(tarea);
}
}
return ResponseEntity.ok(resultado);
}
- Crea dos proyectos y una tarea vinculada al primero. Consulta las tareas de ambos: el primero debe devolver esa tarea y el segundo
[]. - Borra el segundo proyecto y repite su consulta anidada: ahora debe responder
404. Si devuelve[], falta comprobar que el proyecto existe. Si ambos proyectos devuelven la misma tarea, revisa el filtro porproyectoId.
Paso 4 · Automatizar la relación y completar la cobertura — 30 minutos
- Añade a la colección las altas de los dos proyectos. Captura sus ids en variables distintas,
proyectoIdyproyectoVacioId, siguiendo los scripts de la sesión 7. - En el JSON del alta de tarea usa
"proyectoId": {{proyectoId}}. Las llaves se sustituyen antes del envío; sin comillas exteriores el valor será un número JSON. - Comprueba en la consulta del primer proyecto el estado
200, la longitud del array y que la tarea tenga tanto el id esperado como elproyectoIdcorrecto. No basta con comprobar que hay algún elemento. - Comprueba
200y array vacío para el segundo proyecto. Bórralo y comprueba404en su consulta anidada. - Limpia primero la tarea creada y después su proyecto. Ejecuta la colección dos veces seguidas y una vez después de reiniciar. Cada ejecución prepara sus propios datos.
- Completa las comprobaciones de ambas entidades con esta tabla. Una fila puede requerir varias peticiones. Reutiliza las ya guardadas; añade únicamente los casos que faltan.
| Operación | Resultado que debes comprobar |
|---|---|
| Listado y filtro opcional | 200; todos sin filtro, solo coincidencias con filtro y [] si no hay resultados |
| Detalle | 200 con el id solicitado; 404 si no existe |
| POST | 201, id asignado por el servidor y Location utilizable |
| PUT | 200 y sustitución de los campos editables; 404 si no existe |
| PATCH de los campos admitidos | 200, cambio solicitado y conservación del resto; 404 si no existe |
| DELETE | 204 sin cuerpo; el recurso deja de aparecer al consultar |
| Consulta de la relación | Padre con elementos, padre vacío y padre inexistente diferenciados |
Mantén el PATCH limitado a los campos que implementaste en la sesión 6. El booleano opcional y la separación entre datos de entrada y salida se trabajan en la UD3.
Paso 5 · Reproducir el resultado desde otra copia — 25 minutos
- Actualiza las instrucciones del repositorio: requisitos, comando de arranque, selección del entorno y ejecución de la colección. Incluye que los datos se pierden al reiniciar.
- Explica brevemente las decisiones que afectan al cliente:
DELETEdevuelve204incluso si no existe; el servidor asigna el id al crear y la ruta determina el id al actualizar; Jackson queda en modo estricto; una relación vacía devuelve[]. - Guarda la colección y los cambios en tu rama, súbelos y abre o actualiza la PR. En otra carpeta clona el repositorio y selecciona esa rama, ya que puede no estar integrada todavía.
- Arranca esa copia siguiendo sus instrucciones. Detén antes la primera aplicación para liberar el puerto 8080. Ejecuta la colección desde los archivos versionados.
- Corrige cualquier paso que dependiese de una configuración no incluida en el repositorio. En la revisión de la PR, reproduce al menos una petición y contrasta la respuesta con el contrato.
Paso 6 · Identificar lo que falta — 20 minutos
Haz estas pruebas por separado de la colección de aceptación, que debe seguir reflejando el contrato actual. Anota el resultado real y la mejora necesaria. Limpia los registros de prueba al acabar.
| Prueba | Limitación actual | Siguiente trabajo |
|---|---|---|
Crear con {} |
Puede guardar datos incompletos | Validación en la UD3 |
| Crear con una prioridad no prevista | No comprueba valores de negocio | Validación en la UD3 |
| Crear con un id de padre inexistente | No comprueba aún la referencia al guardar | Comprobación de la relación en la UD3 |
Observar un 400 |
El error aún no sigue un formato propio | Errores comunes en la UD3 |
| Añadir un campo interno con getter | Puede aparecer en el JSON público | DTO en la UD3 |
| Reiniciar | Se pierden los registros | Persistencia en la UD5 |
Ampliación si has completado el trabajo
Añade una tarea al segundo proyecto y comprueba que las consultas de ambos siguen separadas. Modifica después la referencia de una tarea mediante un PUT completo: debe desaparecer de una consulta y aparecer en la otra. Incorpora estas comprobaciones a la colección y conserva la limpieza de los datos.
Cierre
15 minutos · resultado comprobable y explicación individual
Al terminar la sesión: ambas entidades mantienen su CRUD, los controladores comparten la misma memoria y la consulta de la relación distingue los tres casos previstos. La colección se puede repetir desde otra copia del repositorio. Puedes explicar dónde se comprueba la existencia del padre, cómo se filtran sus elementos y qué limitaciones siguen pendientes.
- ¿Qué ocurriría si cada controlador construyese su propia
MemoriaProyectoconnew? - ¿Quién llama al constructor de tu controlador, y desde cuándo viene ocurriendo eso?
- Si
getTareas()devolviera una copia de la lista en lugar de la lista, ¿qué petición dejaría de funcionar y cuál seguiría pareciendo correcta? - ¿Por qué la subcolección de un proyecto inexistente no puede responder
200con[]?