Volver a Spring Boot Básico
JPA y Acceso a Base de Datos con Spring Data
Spring Data JPA elimina el código repetitivo para operaciones de base de datos proporcionando repositorios con métodos CRUD integrados.
Mapeo de Entidades
package com.ejemplo.demo.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "productos")
public class Producto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String nombre;
@Column(precision = 10, scale = 2)
private BigDecimal precio;
@Enumerated(EnumType.STRING)
private Categoria categoria;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "proveedor_id")
private Proveedor proveedor;
@CreationTimestamp
private LocalDateTime creadoEn;
@UpdateTimestamp
private LocalDateTime actualizadoEn;
// constructores, getters, setters
}
Relaciones
// Uno a Muchos
@Entity
public class Pedido {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ItemPedido> items = new ArrayList<>();
}
@Entity
public class ItemPedido {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "pedido_id")
private Pedido pedido;
private Integer cantidad;
private BigDecimal precio;
}
// Muchos a Muchos
@Entity
public class Estudiante {
@ManyToMany
@JoinTable(
name = "estudiante_curso",
joinColumns = @JoinColumn(name = "estudiante_id"),
inverseJoinColumns = @JoinColumn(name = "curso_id")
)
private Set<Curso> cursos = new HashSet<>();
}
Repositorio Spring Data
// Repositorio CRUD básico
public interface ProductoRepositorio extends JpaRepository<Producto, Long> {
// Consultas por nombre de método (SQL auto-generado)
List<Producto> findByCategoria(Categoria categoria);
List<Producto> findByPrecioLessThan(BigDecimal precio);
Optional<Producto> findByNombreIgnoreCase(String nombre);
boolean existsByNombre(String nombre);
// Consulta derivada con múltiples condiciones
List<Producto> findByCategoriaAndPrecioGreaterThanOrderByNombreAsc(
Categoria categoria, BigDecimal precioMinimo);
}
Consultas Personalizadas con @Query
public interface ProductoRepositorio extends JpaRepository<Producto, Long> {
// JPQL
@Query("SELECT p FROM Producto p WHERE p.precio BETWEEN :min AND :max")
List<Producto> buscarEnRangoPrecio(@Param("min") BigDecimal min,
@Param("max") BigDecimal max);
// SQL nativo
@Query(value = "SELECT * FROM productos WHERE categoria = :cat LIMIT :limite",
nativeQuery = true)
List<Producto> buscarTopPorCategoria(@Param("cat") String categoria,
@Param("limite") int limite);
// Consulta modificadora
@Modifying
@Transactional
@Query("UPDATE Producto p SET p.precio = p.precio * :factor WHERE p.categoria = :cat")
int ajustarPrecioPorCategoria(@Param("factor") BigDecimal factor,
@Param("cat") Categoria cat);
}
Paginación y Ordenamiento
// Repositorio con Pageable
Page<Producto> findByCategoria(Categoria categoria, Pageable pageable);
// Uso en el servicio
@Service
public class ProductoServicio {
public Page<Producto> obtenerProductos(int pagina, int tamaño, String ordenarPor) {
Pageable pageable = PageRequest.of(pagina, tamaño, Sort.by(ordenarPor).ascending());
return productoRepositorio.findAll(pageable);
}
}
// Controlador
@GetMapping
public Page<ProductoResponse> obtenerTodos(
@RequestParam(defaultValue = "0") int pagina,
@RequestParam(defaultValue = "10") int tamaño,
@RequestParam(defaultValue = "nombre") String orden) {
return productoServicio.obtenerProductos(pagina, tamaño, orden)
.map(ProductoResponse::desde);
}
Capa de Servicio con Transacciones
@Service
@Transactional
public class PedidoServicio {
public Pedido crearPedido(CrearPedidoRequest request) {
Pedido pedido = new Pedido();
pedido.setClienteId(request.getClienteId());
for (ItemPedidoRequest item : request.getItems()) {
Producto producto = productoRepositorio.findById(item.getProductoId())
.orElseThrow(() -> new RecursoNoEncontradoException("Producto", item.getProductoId()));
if (producto.getStock() < item.getCantidad()) {
throw new StockInsuficienteException(producto.getNombre());
}
producto.setStock(producto.getStock() - item.getCantidad());
pedido.agregarItem(producto, item.getCantidad());
}
return pedidoRepositorio.save(pedido);
}
@Transactional(readOnly = true)
public Pedido buscarPorId(Long id) {
return pedidoRepositorio.findById(id)
.orElseThrow(() -> new RecursoNoEncontradoException("Pedido", id));
}
}
Migración de BD con Flyway
<!-- dependencia en pom.xml -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
-- src/main/resources/db/migration/V1__esquema_inicial.sql
CREATE TABLE productos (
id BIGSERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
precio NUMERIC(10,2),
categoria VARCHAR(50),
creado_en TIMESTAMP DEFAULT NOW()
);
-- src/main/resources/db/migration/V2__agregar_proveedor.sql
ALTER TABLE productos ADD COLUMN proveedor_id BIGINT;
CREATE TABLE proveedores (
id BIGSERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL
);
ALTER TABLE productos ADD FOREIGN KEY (proveedor_id) REFERENCES proveedores(id);
H2 Base de Datos en Memoria (Pruebas)
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
# application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create-drop