Saltar al contenido principal

Spring Data JPA

En la Fase 1, TaskRepository era una interfaz con una implementación (InMemoryTaskRepository) que tú escribías a mano, usando un ConcurrentHashMap. Con Spring Data JPA, esa implementación deja de hacer falta.

La entidad: de record a @Entity​

En la Fase 1, Task era un record inmutable. Una entidad JPA no puede serlo: necesita un constructor sin argumentos (JPA lo usa internamente para reconstruir objetos desde la base de datos) y campos mutables (para que Hibernate pueda actualizar sus valores).

@Entity
@Table(name = "tasks")
public class Task {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String titulo;

private String descripcion;

@Column(nullable = false)
private boolean completada;

protected Task() {
}

public Task(String titulo, String descripcion, boolean completada) {
this.titulo = titulo;
this.descripcion = descripcion;
this.completada = completada;
}

// getters y setters
}
  • @Entity + @Table(name = "tasks") — le dice a JPA que esta clase se mapea a la tabla tasks (la que crea la migración de Flyway).
  • @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) — el campo id es la clave primaria, generada por la base de datos (coincide con GENERATED BY DEFAULT AS IDENTITY en el SQL de la migración).
  • El constructor protegido sin argumentos es un requisito técnico de JPA, no se usa directamente desde tu código.

El repositorio: cero implementación​

public interface TaskRepository extends JpaRepository<Task, Long> {
}

Eso es todo. JpaRepository<Task, Long> ya trae save, findAll, findById, delete y muchos más métodos, implementados por Spring Data en tiempo de ejecución — nunca escribes una clase TaskRepositoryImpl. Comparado con InMemoryTaskRepository de la Fase 1 (una clase completa con un mapa, un contador atómico y cuatro métodos), esta interfaz vacía hace exactamente el mismo trabajo, pero contra una base de datos real.

Esto es precisamente lo que hacía posible la inyección de dependencias de la Fase 1: TaskServiceImpl sigue dependiendo de la interfaz TaskRepository, nunca de una implementación concreta — así que pudimos cambiar completamente cómo se guardan los datos sin tocar el service.

@Transactional​

Los métodos de escritura del service ahora se anotan con @Transactional:

@Override
@Transactional
public Task create(TaskRequest request) {
Task task = new Task(request.titulo(), request.descripcion(), request.completada());
return taskRepository.save(task);
}

Con una base de datos real, una operación puede implicar varias sentencias SQL que deben tener éxito juntas o fallar juntas (por ejemplo, si más adelante create también tuviera que escribir en otra tabla relacionada). @Transactional envuelve el método en una transacción: si algo falla a mitad, todo se revierte. Con el repositorio en memoria de la Fase 1 esto no hacía falta — no había nada que revertir.

open-in-view: false​

Spring Boot activa por defecto un mecanismo llamado Open Session in View: mantiene abierta la conexión con la base de datos durante toda la petición HTTP, incluso después de que el service haya terminado y mientras Jackson convierte la respuesta a JSON. Por eso al arrancar verás este aviso si no lo configuras:

spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering.

Parece cómodo — una relación perezosa (LAZY) se puede cargar desde el controlador sin que falle —, pero tiene dos costes reales: cada petición retiene una conexión del pool más tiempo del necesario, y pueden aparecer consultas SQL "invisibles" disparadas desde el controlador o desde la serialización, lejos de donde nadie las espera. Todos los ejemplos de este sitio lo desactivan:

spring:
jpa:
open-in-view: false

Con eso, el acceso a la base de datos queda donde debe estar: dentro de los métodos @Transactional del service. La consecuencia práctica es una regla que ya siguen los ejemplos: el service devuelve lo que el controlador necesita ya cargado (a partir de la Fase 3, un DTO como TaskResponse), en vez de entregar una entidad con relaciones sin cargar para que alguien las recorra después.