Ir al contenido

Release 6: Persistencia real

Objetivo: diseñar e implementar la persistencia real unificada con JPA e H2 del proyecto. Al terminar, la aplicación mantiene su estado entre reinicios: las tres entidades del módulo están respaldadas por tablas reales, con las relaciones intra-módulo e inter-módulo mapeadas según el diagrama de clases consolidado y acordado por el equipo, y con datos iniciales cargados desde data.sql.


Paso 1: Acordar el modelo consolidado del proyecto

Sección titulada «Paso 1: Acordar el modelo consolidado del proyecto»

Antes de que cualquier integrante escriba una línea de JPA, el equipo completa este paso en conjunto: consolida los modelos de todos los módulos en un solo diagrama de clases UML y precisa cada asociación inter-módulo en una tabla (cardinalidad, dirección, anotación de cada extremo, nombre del atributo mappedBy del lado inverso y el nombre del responsable de escribirla, un integrante por asociación). Si dos integrantes modelan sus asociaciones inter-módulo de forma independiente, los conflictos al compilar son inevitables.

Cómo se hace, paso a paso, está en el Taller: acordar el modelo consolidado del proyecto, que se trabaja en clase, en equipo.

El resultado, el diagrama de clases consolidado y la tabla de asociaciones, debe quedar publicado en la wiki del repositorio del grupo. Nadie empieza la implementación hasta que estén aprobados por todos.


Paso 2: Construir la capa de persistencia (entidades y repositories)

Sección titulada «Paso 2: Construir la capa de persistencia (entidades y repositories)»

Con el diagrama consolidado y aprobado, cada integrante construye la persistencia de las tres entidades de su módulo.

Clases compartidas del grupo. Tres clases son de todo el grupo y se crean una sola vez, no una por módulo: si cada integrante crea la suya, el repositorio queda con una BaseEntity por módulo y, con CorsConfig o ModelMapperConfig repetidas, el back no arranca por nombres de bean duplicados. Viven en dos paquetes nuevos, hermanos de los módulos y bajo el paquete base co.edu.uniandes.<nombreproyecto>:

  • common: BaseEntity.
  • config: ModelMapperConfig y CorsConfig (esta ya existe desde el Release 2, dentro de un módulo, y se mueve).

Para que Spring encuentre los paquetes config y los demás módulos, la clase @SpringBootApplication tiene que estar en el paquete base. Si se queda dentro del paquete de un módulo, el back arranca sin error pero los otros módulos no responden. Un integrante, el que tenga el turno de archivos compartidos como en el Release 5, abre un PR pequeño antes de que los demás sigan: mueve la clase principal al paquete base, crea common/BaseEntity, crea config/ModelMapperConfig con su dependencia en el pom.xml, y mueve CorsConfig a config. Cuando se fusiona, los demás hacen git pull, borran las copias que hayan creado en su módulo y siguen. Los pasos exactos están en el tutorial de la primera entidad y en el de repository.

Entidades JPA. Por cada entidad, crear la clase que JPA mapea a una tabla, siguiendo las convenciones de Persistencia con JPA (1), Persistencia con JPA (2): mapeo de asociaciones y el tutorial Crear tu primera entidad JPA: extiende BaseEntity (la del paquete common), se llama igual que el DTO sin el sufijo DTO más el sufijo Entity (ArtistaDTO → ArtistaEntity), y no necesita @Table salvo que el nombre por defecto no sirva. Las relaciones intra-módulo siguen el patrón @OneToMany(mappedBy = "...") en el lado “uno” y @ManyToOne en el lado “muchos”. cascade no se agrega por defecto: solo cuando la relación es una composición (ver la nota de abajo) (ver el tutorial Mapear una asociación OneToMany para el detalle de la llave foránea). Si la relación es muchos a muchos, ver el tutorial Mapear una asociación ManyToMany. Las relaciones inter-módulo siguen exactamente lo acordado en el Paso 1 (el diagrama de clases y la tabla de asociaciones de la wiki): cardinalidad, dirección y lado propietario ya están decididos, no se vuelven a discutir acá.

Repositories. Por cada entidad, la interfaz de repositorio pasa de declarar sus propios métodos a extender JpaRepository<Entidad, Long>. La clase InMemory<Entidad>Repository se elimina: ya no hace falta una implementación propia, JpaRepository la provee. Esto aplica también a las entidades nuevas de este release (por ejemplo álbum y canción), que no tenían versión en memoria: no hay nada que reemplazar, basta crear la interfaz. El tutorial Convertir tu repository a JpaRepository lo muestra paso a paso para el artista, y el de @OneToMany crea los de álbum y canción.

Controller. Al devolver entidades en vez de DTOs, el repository hace que el GET del Controller deje de compilar. Se corrige usando el ModelMapper compartido del grupo (ver arriba) y convirtiendo cada entidad a su <Entidad>DTO dentro del Controller, que nunca retorna una entidad directamente. El POST no cambia: guardar de verdad llega con el Service en el Release 7. Este paso también está en el tutorial anterior.


Para que la aplicación no arranque vacía (ver el tutorial Cargar datos iniciales con data.sql), agregar un archivo src/main/resources/data.sql con un INSERT por cada registro de las tres entidades del módulo, y agregar en application.properties la línea spring.jpa.defer-datasource-initialization=true. El orden importa: las tablas sin llave foránea van primero, y las que dependen de otra tabla (por ejemplo, canciones que referencian un álbum) van después, con el id correcto de la fila de la que dependen.

Como data.sql es un solo archivo compartido por todo el equipo, conviene probar primero con datos propios antes de tocarlo: cada integrante escribe sus INSERT de prueba en un archivo aparte (por ejemplo, artista.sql) y los corre a mano en la consola de H2. De ahí se pasa al data.sql del equipo de una sola vez cuando el módulo esté listo, o agregando cada entidad al archivo compartido en el orden en que se va terminando, siempre respetando las llaves foráneas. Para los campos que son URL de imagen, usar el link de una imagen vacía o de marcador de posición en vez de uno roto.

IA permitida aquí: se puede pedir a Gemini que genere los INSERT a partir de los datos que tenía el InMemoryRepository (el JSON del Gist). Para que acierte con los nombres reales de tablas y columnas, el prompt debe incluir también el esquema descargado con SCRIPT (tutorial de la primera entidad JPA), no solo los datos. El estudiante verifica que el orden respeta las llaves foráneas y que los ids son consistentes entre tablas.


  • Las entidades JPA con asociaciones inter-módulo solo se escriben después de que el diagrama de la wiki esté aprobado por todos.
  • La entidad dueña de una relación inter-módulo (la que contiene la llave foránea: el lado @ManyToOne o, en una relación muchos a muchos, el que declara @JoinTable) la escribe el integrante cuyo módulo contiene esa entidad. El otro integrante agrega el lado inverso en su propio PR.
  • Los PRs se mergean uno a la vez, igual que en R5.
  • Si el pipeline falla después de un merge, máxima prioridad del equipo: se revierte y se investiga en rama aparte.

Back:

  • Diagrama del modelo completo de clases (UML) del grupo publicado en la wiki.
  • Tabla en la wiki donde se precisan las asociaciones.
  • Paquetes compartidos common (con BaseEntity) y config (con ModelMapperConfig y CorsConfig), una sola copia por grupo, con la clase @SpringBootApplication en el paquete base.
  • Entidades JPA de las tres entidades del módulo, con anotaciones de relaciones intra-módulo e inter-módulo según el diagrama acordado.
  • Interfaz <Entidad>Repository extendiendo JpaRepository para cada entidad. InMemory<Entidad>Repository eliminado.
  • Controller con ModelMapper: el GET de cada entidad lee de la base de datos y responde DTOs.
  • Datos iniciales en data.sql.