5. Repositorios y Acceso a Datos
🎯 Objetivos
En esta sección aprenderás a: - Entender qué es Spring Data JPA - Crear repositorios con JpaRepository - Implementar operaciones CRUD automáticas - Crear consultas derivadas de nombres de métodos - Aplicar el patrón Repository - Configurar consultas personalizadas con @Query
📋 Prerrequisitos
- Entidades JPA creadas
- Configuración de base de datos completada
- Conocimientos básicos de JPA/Hibernate
- Comprensión de interfaces en Java
🗄️ ¿Qué es Spring Data JPA?
Spring Data JPA es una abstracción que simplifica el acceso a datos proporcionando:
Características Principales
- Implementación automática: No necesitas escribir código de implementación
- Métodos CRUD predefinidos: Operaciones básicas ya incluidas
- Consultas derivadas: Genera consultas basadas en nombres de métodos
- Soporte para paginación: Manejo automático de grandes conjuntos de datos
- Auditoría: Seguimiento automático de cambios
- Transacciones: Gestión automática de transacciones
Ventajas de JpaRepository
public interface UserRepository extends JpaRepository<User, Long> {
// ¡No necesitas implementar nada!
// Spring Data JPA genera automáticamente:
// - findAll()
// - findById(Long id)
// - save(User user)
// - deleteById(Long id)
// - count()
// - existsById(Long id)
// Y muchos más...
}
Jerarquía de Interfaces
Repository<T, ID>
↓
CrudRepository<T, ID>
↓
PagingAndSortingRepository<T, ID>
↓
JpaRepository<T, ID>
📁 Estructura de Repositorios
Crear el Paquete
Primero, crea la estructura de carpetas:
src/main/java/com/example/pib2/
└── repositories/
├── UserRepository.java
├── ItemRepository.java
├── LoanRepository.java
└── LoanHistoryRepository.java
👤 UserRepository (Repositorio de Usuarios)
Crea el archivo src/main/java/com/example/pib2/repositories/UserRepository.java:
package com.example.pib2.repositories;
import com.example.pib2.models.entities.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// ========================================
// MÉTODOS AUTOMÁTICOS HEREDADOS
// ========================================
// List<User> findAll()
// Optional<User> findById(Long id)
// User save(User user)
// void deleteById(Long id)
// long count()
// boolean existsById(Long id)
// void delete(User user)
// void deleteAll()
// ========================================
// CONSULTAS DERIVADAS
// ========================================
// Buscar por username exacto
Optional<User> findByUsername(String username);
// Buscar por email exacto
Optional<User> findByEmail(String email);
// Buscar por rol
List<User> findByRole(String role);
// Buscar por username que contenga texto (ignorando mayúsculas)
List<User> findByUsernameContainingIgnoreCase(String username);
// Verificar si existe username
boolean existsByUsername(String username);
// Verificar si existe email
boolean existsByEmail(String email);
// Buscar por username y email
Optional<User> findByUsernameAndEmail(String username, String email);
// ========================================
// CONSULTAS PERSONALIZADAS CON @Query
// ========================================
@Query("SELECT u FROM User u WHERE u.role = :role AND u.username LIKE %:username%")
List<User> findUsersByRoleAndUsername(@Param("role") String role, @Param("username") String username);
@Query("SELECT COUNT(u) FROM User u WHERE u.role = :role")
long countByRole(@Param("role") String role);
// Consulta nativa SQL
@Query(value = "SELECT * FROM users WHERE created_at > NOW() - INTERVAL 30 DAY", nativeQuery = true)
List<User> findRecentUsers();
}
🔍 Análisis del UserRepository
Herencia de JpaRepository
JpaRepository<User, Long>
// ↑ ↑
// Entidad Tipo del ID
Métodos Automáticos Disponibles
| Método | Descripción | Ejemplo de Uso |
|---|---|---|
findAll() |
Obtiene todos los usuarios | List<User> users = userRepository.findAll(); |
findById(Long id) |
Busca por ID | Optional<User> user = userRepository.findById(1L); |
save(User user) |
Guarda o actualiza | User saved = userRepository.save(user); |
deleteById(Long id) |
Elimina por ID | userRepository.deleteById(1L); |
count() |
Cuenta registros | long total = userRepository.count(); |
existsById(Long id) |
Verifica existencia | boolean exists = userRepository.existsById(1L); |
📦 ItemRepository (Repositorio de Artículos)
Crea el archivo src/main/java/com/example/pib2/repositories/ItemRepository.java:
package com.example.pib2.repositories;
import com.example.pib2.models.entities.Item;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ItemRepository extends JpaRepository<Item, Long> {
// ========================================
// CONSULTAS DERIVADAS
// ========================================
// Buscar por nombre exacto
Optional<Item> findByName(String name);
// Buscar por nombre que contenga texto
List<Item> findByNameContaining(String name);
// Buscar por nombre ignorando mayúsculas
List<Item> findByNameContainingIgnoreCase(String name);
// Buscar por cantidad mayor que
List<Item> findByQuantityGreaterThan(int quantity);
// Buscar por cantidad menor que
List<Item> findByQuantityLessThan(int quantity);
// Buscar por cantidad entre valores
List<Item> findByQuantityBetween(int min, int max);
// Buscar por nombre y cantidad
List<Item> findByNameAndQuantityGreaterThan(String name, int quantity);
// Buscar por descripción que contenga texto (ignorando mayúsculas)
List<Item> findByDescriptionContainingIgnoreCase(String description);
// Contar items con cantidad menor que
long countByQuantityLessThan(int quantity);
// Verificar si existe item con nombre
boolean existsByName(String name);
// Buscar items disponibles (cantidad > 0)
List<Item> findByQuantityGreaterThanOrderByNameAsc(int quantity);
// ========================================
// CONSULTAS PERSONALIZADAS
// ========================================
@Query("SELECT i FROM Item i WHERE i.quantity > 0 AND i.name LIKE %:searchTerm%")
List<Item> findAvailableItemsByName(@Param("searchTerm") String searchTerm);
@Query("SELECT i FROM Item i WHERE i.quantity < :threshold ORDER BY i.quantity ASC")
List<Item> findLowStockItems(@Param("threshold") int threshold);
@Query("SELECT SUM(i.quantity) FROM Item i")
Long getTotalQuantity();
// Consulta nativa para estadísticas
@Query(value = "SELECT AVG(quantity) FROM items", nativeQuery = true)
Double getAverageQuantity();
}
📋 LoanRepository (Repositorio de Préstamos)
Crea el archivo src/main/java/com/example/pib2/repositories/LoanRepository.java:
package com.example.pib2.repositories;
import com.example.pib2.models.entities.Loan;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface LoanRepository extends JpaRepository<Loan, Long> {
// ========================================
// CONSULTAS DERIVADAS
// ========================================
// Buscar préstamos por usuario
List<Loan> findByUserId(Long userId);
// Buscar préstamos por artículo
List<Loan> findByItemId(Long itemId);
// Buscar préstamos devueltos/no devueltos
List<Loan> findByReturned(boolean returned);
// Buscar préstamos activos de un usuario
List<Loan> findByUserIdAndReturned(Long userId, boolean returned);
// Buscar préstamos por fecha de préstamo
List<Loan> findByLoanDateBetween(LocalDateTime start, LocalDateTime end);
// Buscar préstamos vencidos (fecha de devolución pasada y no devueltos)
List<Loan> findByReturnDateBeforeAndReturnedFalse(LocalDateTime date);
// Contar préstamos activos de un usuario
long countByUserIdAndReturnedFalse(Long userId);
// Verificar si un usuario tiene préstamos activos
boolean existsByUserIdAndReturnedFalse(Long userId);
// Verificar si un artículo está prestado
boolean existsByItemIdAndReturnedFalse(Long itemId);
// ========================================
// CONSULTAS PERSONALIZADAS
// ========================================
@Query("SELECT l FROM Loan l WHERE l.user.id = :userId AND l.returned = false")
List<Loan> findActiveLoansByUser(@Param("userId") Long userId);
@Query("SELECT l FROM Loan l WHERE l.returnDate < :currentDate AND l.returned = false")
List<Loan> findOverdueLoans(@Param("currentDate") LocalDateTime currentDate);
@Query("SELECT COUNT(l) FROM Loan l WHERE l.user.id = :userId")
long countTotalLoansByUser(@Param("userId") Long userId);
@Query("SELECT l FROM Loan l JOIN FETCH l.user JOIN FETCH l.item WHERE l.returned = false")
List<Loan> findActiveLoansWithDetails();
// Estadísticas
@Query("SELECT COUNT(l) FROM Loan l WHERE l.loanDate >= :startDate")
long countLoansFromDate(@Param("startDate") LocalDateTime startDate);
}
📊 LoanHistoryRepository (Repositorio de Historial)
Crea el archivo src/main/java/com/example/pib2/repositories/LoanHistoryRepository.java:
package com.example.pib2.repositories;
import com.example.pib2.models.entities.LoanHistory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface LoanHistoryRepository extends JpaRepository<LoanHistory, Long> {
// ========================================
// CONSULTAS DERIVADAS
// ========================================
// Buscar historial por préstamo
List<LoanHistory> findByLoanId(Long loanId);
// Buscar por acción específica
List<LoanHistory> findByAction(String action);
// Buscar por rango de fechas
List<LoanHistory> findByActionDateBetween(LocalDateTime start, LocalDateTime end);
// Buscar por préstamo y acción
List<LoanHistory> findByLoanIdAndAction(Long loanId, String action);
// Buscar historial ordenado por fecha
List<LoanHistory> findByLoanIdOrderByActionDateDesc(Long loanId);
// Buscar acciones recientes
List<LoanHistory> findByActionDateAfterOrderByActionDateDesc(LocalDateTime date);
// ========================================
// CONSULTAS PERSONALIZADAS
// ========================================
@Query("SELECT lh FROM LoanHistory lh WHERE lh.loan.user.id = :userId ORDER BY lh.actionDate DESC")
List<LoanHistory> findHistoryByUser(@Param("userId") Long userId);
@Query("SELECT lh FROM LoanHistory lh WHERE lh.action = :action AND lh.actionDate >= :fromDate")
List<LoanHistory> findRecentActionHistory(@Param("action") String action, @Param("fromDate") LocalDateTime fromDate);
@Query("SELECT COUNT(lh) FROM LoanHistory lh WHERE lh.action = :action AND lh.actionDate >= :fromDate")
long countActionsSince(@Param("action") String action, @Param("fromDate") LocalDateTime fromDate);
// Auditoría completa de un préstamo
@Query("SELECT lh FROM LoanHistory lh JOIN FETCH lh.loan WHERE lh.loan.id = :loanId ORDER BY lh.actionDate ASC")
List<LoanHistory> findCompleteAuditTrail(@Param("loanId") Long loanId);
}
🔍 Consultas Derivadas - Palabras Clave
Palabras Clave Principales
| Palabra Clave | Descripción | Ejemplo |
|---|---|---|
findBy |
Buscar registros | findByUsername(String username) |
countBy |
Contar registros | countByRole(String role) |
existsBy |
Verificar existencia | existsByEmail(String email) |
deleteBy |
Eliminar registros | deleteByUsername(String username) |
Operadores de Comparación
| Operador | Descripción | Ejemplo |
|---|---|---|
GreaterThan |
Mayor que | findByQuantityGreaterThan(int quantity) |
LessThan |
Menor que | findByQuantityLessThan(int quantity) |
Between |
Entre valores | findByQuantityBetween(int min, int max) |
Like |
Coincidencia parcial | findByNameLike(String pattern) |
Containing |
Contiene texto | findByNameContaining(String text) |
IgnoreCase |
Ignorar mayúsculas | findByNameIgnoreCase(String name) |
OrderBy |
Ordenar resultados | findByRoleOrderByUsernameAsc(String role) |
Operadores Lógicos
| Operador | Descripción | Ejemplo |
|---|---|---|
And |
Y lógico | findByUsernameAndEmail(String username, String email) |
Or |
O lógico | findByUsernameOrEmail(String username, String email) |
Not |
Negación | findByUsernameNot(String username) |
In |
En lista | findByRoleIn(List<String> roles) |
NotIn |
No en lista | findByRoleNotIn(List<String> roles) |
📝 Consultas Personalizadas con @Query
JPQL (Java Persistence Query Language)
// Consulta JPQL básica
@Query("SELECT u FROM User u WHERE u.role = :role")
List<User> findUsersByRole(@Param("role") String role);
// Consulta JPQL con JOIN
@Query("SELECT l FROM Loan l JOIN FETCH l.user JOIN FETCH l.item WHERE l.returned = false")
List<Loan> findActiveLoansWithDetails();
// Consulta JPQL con funciones agregadas
@Query("SELECT COUNT(u) FROM User u WHERE u.role = :role")
long countByRole(@Param("role") String role);
SQL Nativo
// Consulta SQL nativa
@Query(value = "SELECT * FROM users WHERE created_at > NOW() - INTERVAL 30 DAY", nativeQuery = true)
List<User> findRecentUsers();
// Consulta SQL nativa con parámetros
@Query(value = "SELECT AVG(quantity) FROM items WHERE name LIKE %:name%", nativeQuery = true)
Double getAverageQuantityByName(@Param("name") String name);
✅ Verificación de Repositorios
1. Verificar Estructura de Archivos
# Verificar que los archivos existen
ls src/main/java/com/example/pib2/repositories/
Deberías ver:
UserRepository.java
ItemRepository.java
LoanRepository.java
LoanHistoryRepository.java
2. Compilar el Proyecto
# Compilar para verificar sintaxis
./mvnw compile
3. Ejecutar la Aplicación
# Ejecutar la aplicación
./mvnw spring-boot:run
4. Verificar en Logs
Busca en los logs mensajes como:
Hibernate: create table users (...)
Hibernate: create table items (...)
Hibernate: create table loans (...)
Hibernate: create table loan_history (...)
🚨 Problemas Comunes y Soluciones
Error: "No qualifying bean of type repository found"
Problema: Spring no encuentra el repositorio
Solución:
// Agregar @Repository a la interfaz
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// ...
}
// O habilitar escaneo de repositorios en la clase principal
@SpringBootApplication
@EnableJpaRepositories("com.example.pib2.repositories")
public class Pib2Application {
// ...
}
Error: "Invalid derived query"
Problema: Nombre de método incorrecto
Solución:
// ❌ Incorrecto
List<User> findByUserName(String username); // Campo se llama 'username', no 'userName'
// ✅ Correcto
List<User> findByUsername(String username);
Error: "Could not resolve parameter"
Problema: Falta @Param en consulta personalizada
Solución:
// ❌ Incorrecto
@Query("SELECT u FROM User u WHERE u.role = :role")
List<User> findUsersByRole(String role);
// ✅ Correcto
@Query("SELECT u FROM User u WHERE u.role = :role")
List<User> findUsersByRole(@Param("role") String role);
🎯 Mejores Prácticas
1. Nomenclatura Consistente
// ✅ Bueno: Nombres descriptivos
List<User> findByUsernameContainingIgnoreCase(String username);
boolean existsByEmailAndUsernameNot(String email, String username);
// ❌ Malo: Nombres ambiguos
List<User> findByName(String name); // ¿username o fullName?
List<User> findUsers(String param); // ¿qué parámetro?
2. Usar Optional para Resultados Únicos
// ✅ Bueno: Manejo seguro de nulos
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
// ❌ Malo: Puede retornar null
User findByUsername(String username);
3. Consultas Eficientes
// ✅ Bueno: JOIN FETCH para evitar N+1
@Query("SELECT l FROM Loan l JOIN FETCH l.user JOIN FETCH l.item")
List<Loan> findAllWithDetails();
// ❌ Malo: Carga perezosa puede causar N+1
List<Loan> findAll(); // Luego acceder a loan.getUser().getUsername()
4. Validación de Parámetros
// En el servicio que usa el repositorio
public Optional<User> findByUsername(String username) {
if (username == null || username.trim().isEmpty()) {
return Optional.empty();
}
return userRepository.findByUsername(username.trim());
}
5. Documentación de Consultas Complejas
/**
* Encuentra préstamos vencidos que no han sido devueltos.
* Un préstamo se considera vencido si la fecha de devolución
* es anterior a la fecha actual y el campo 'returned' es false.
*
* @param currentDate fecha actual para comparar
* @return lista de préstamos vencidos
*/
@Query("SELECT l FROM Loan l WHERE l.returnDate < :currentDate AND l.returned = false")
List<Loan> findOverdueLoans(@Param("currentDate") LocalDateTime currentDate);
🔑 Conceptos Clave Aprendidos
- Spring Data JPA: Abstracción que simplifica el acceso a datos
- JpaRepository: Interfaz que proporciona métodos CRUD automáticos
- Consultas Derivadas: Generación automática basada en nombres de métodos
- @Query: Consultas personalizadas con JPQL o SQL nativo
- @Repository: Anotación para marcar componentes de acceso a datos
- Optional: Manejo seguro de resultados que pueden ser nulos
- JOIN FETCH: Optimización para evitar el problema N+1
🚀 Próximos Pasos
En el siguiente tutorial aprenderás sobre: - Servicios y Lógica de Negocio: Implementar la capa de servicios - Inyección de Dependencias: Usar repositorios en servicios - Transacciones: Gestión automática de transacciones - Validaciones de Negocio: Reglas específicas del dominio - Manejo de Excepciones: Gestión de errores en la capa de datos
📚 Recursos Adicionales: - Spring Data JPA Reference - Query Methods - Custom Queries
🔗 Enlaces Relacionados: - ← 4. DTOs y Mapeo de Datos - → 6. Servicios y Lógica de Negocio - 📋 Índice Principal