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