API Specification

Avanzado
SpringBoot
SpringBoot
Actualizado: 13/06/2025

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

Introducción teórica a Specification

El patrón Specification es una técnica de diseño que permite encapsular reglas de negocio complejas en objetos reutilizables y combinables. En el contexto de Spring Data JPA, las Specifications nos proporcionan una forma elegante de construir consultas dinámicas sin recurrir a concatenación manual de strings SQL o JPQL.

¿Qué problema resuelve Specification?

Imagina que necesitas buscar productos en una tienda online con múltiples filtros opcionales: por categoría, rango de precios, marca, disponibilidad, etc. Sin Specification, tendrías que crear múltiples métodos en tu repositorio o construir consultas dinámicas de forma manual:

// Enfoque tradicional - poco escalable
public interface ProductoRepository extends JpaRepository<Producto, Long> {
    List<Producto> findByCategoria(String categoria);
    List<Producto> findByPrecioBetween(BigDecimal min, BigDecimal max);
    List<Producto> findByCategoriaAndPrecioBetween(String categoria, BigDecimal min, BigDecimal max);
    List<Producto> findByCategoriaAndMarcaAndDisponible(String categoria, String marca, Boolean disponible);
    // ... y así sucesivamente para cada combinación posible
}

Este enfoque se vuelve inmanejable rápidamente cuando aumenta el número de filtros posibles.

Conceptos fundamentales de Specification

Una Specification en Spring Data JPA es una interfaz funcional que define un único método:

public interface Specification<T> {
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
}

Los tres parámetros de este método son elementos clave de la Criteria API de JPA:

  • Root: Representa la entidad raíz de la consulta, desde donde accedemos a sus atributos
  • CriteriaQuery<?>: Define la estructura de la consulta (SELECT, FROM, WHERE, etc.)
  • CriteriaBuilder: Proporciona métodos para construir predicados y expresiones

Ventajas del patrón Specification

Las Specifications ofrecen varios beneficios importantes:

  • Reutilización: Una vez definida, una specification puede usarse en múltiples contextos
  • Composición: Se pueden combinar specifications usando operadores lógicos (AND, OR, NOT)
  • Legibilidad: El código expresa claramente la intención de negocio
  • Mantenibilidad: Los cambios en las reglas de filtrado se centralizan en un solo lugar
  • Type safety: Al usar la Criteria API, obtenemos verificación de tipos en tiempo de compilación

Ejemplo básico de Specification

Veamos cómo se vería una specification simple para filtrar productos por categoría:

public class ProductoSpecifications {
    
    public static Specification<Producto> tieneCategoria(String categoria) {
        return (root, query, criteriaBuilder) -> {
            if (categoria == null || categoria.isEmpty()) {
                return criteriaBuilder.conjunction(); // Siempre verdadero
            }
            return criteriaBuilder.equal(root.get("categoria"), categoria);
        };
    }
}

Esta specification encapsula la lógica para filtrar por categoría, manejando el caso donde el parámetro puede ser nulo o vacío.

Composición de Specifications

La verdadera potencia de las Specifications surge cuando las combinamos. Spring Data JPA proporciona métodos estáticos en la interfaz Specification para esto:

// Combinar specifications con AND
Specification<Producto> spec = Specification
    .where(ProductoSpecifications.tieneCategoria("Electrónicos"))
    .and(ProductoSpecifications.precioEntre(100.0, 500.0))
    .and(ProductoSpecifications.estaDisponible());

// Usar OR para alternativas
Specification<Producto> specAlternativa = Specification
    .where(ProductoSpecifications.tieneCategoria("Libros"))
    .or(ProductoSpecifications.tieneCategoria("Música"));

Integración con repositorios

Para usar Specifications en nuestros repositorios, necesitamos que extiendan la interfaz JpaSpecificationExecutor:

public interface ProductoRepository extends JpaRepository<Producto, Long>, 
                                           JpaSpecificationExecutor<Producto> {
    // Los métodos de JpaSpecificationExecutor están disponibles automáticamente
}

Esta interfaz proporciona métodos como findAll(Specification<T> spec), findOne(Specification<T> spec), y count(Specification<T> spec).

Casos de uso ideales

Las Specifications son especialmente útiles en escenarios como:

  • Filtros de búsqueda avanzada en interfaces de usuario
  • Reportes dinámicos donde los criterios cambian según el usuario
  • APIs REST que aceptan múltiples parámetros de filtrado opcionales
  • Consultas complejas que involucran múltiples entidades relacionadas

El patrón Specification transforma consultas complejas y dinámicas en código mantenible y expresivo, eliminando la necesidad de construir consultas mediante concatenación de strings y reduciendo significativamente la cantidad de métodos necesarios en nuestros repositorios.

¿Te está gustando esta lección?

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

Implementar Specification en Spring Data JPA

Ahora que comprendemos la teoría detrás del patrón Specification, es momento de implementarlo en un proyecto real. Veremos cómo configurar nuestro entorno, crear specifications prácticas y utilizarlas en nuestros repositorios.

Configuración inicial del proyecto

Para trabajar con Specifications en Spring Data JPA, necesitamos asegurarnos de tener las dependencias correctas en nuestro pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

Spring Boot incluye automáticamente todo lo necesario para trabajar con Specifications a través del starter de Spring Data JPA.

Creando nuestra entidad de ejemplo

Trabajaremos con una entidad Libro que nos permitirá demostrar diferentes tipos de filtros:

@Entity
@Table(name = "libros")
public class Libro {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String titulo;
    
    @Column(nullable = false)
    private String autor;
    
    @Column(nullable = false)
    private String genero;
    
    @Column(nullable = false)
    private BigDecimal precio;
    
    @Column(name = "fecha_publicacion")
    private LocalDate fechaPublicacion;
    
    @Column(nullable = false)
    private Boolean disponible;
    
    // Constructores, getters y setters
    public Libro() {}
    
    public Libro(String titulo, String autor, String genero, BigDecimal precio, 
                 LocalDate fechaPublicacion, Boolean disponible) {
        this.titulo = titulo;
        this.autor = autor;
        this.genero = genero;
        this.precio = precio;
        this.fechaPublicacion = fechaPublicacion;
        this.disponible = disponible;
    }
    
    // Getters y setters omitidos por brevedad
}

Configurando el repositorio

Nuestro repositorio debe extender JpaSpecificationExecutor para habilitar el uso de Specifications:

@Repository
public interface LibroRepository extends JpaRepository<Libro, Long>, 
                                        JpaSpecificationExecutor<Libro> {
    // No necesitamos definir métodos adicionales
    // JpaSpecificationExecutor proporciona los métodos necesarios
}

Implementando Specifications básicas

Creamos una clase utilitaria que contenga nuestras specifications reutilizables:

public class LibroSpecifications {
    
    public static Specification<Libro> tieneTitulo(String titulo) {
        return (root, query, criteriaBuilder) -> {
            if (titulo == null || titulo.trim().isEmpty()) {
                return criteriaBuilder.conjunction();
            }
            return criteriaBuilder.like(
                criteriaBuilder.lower(root.get("titulo")), 
                "%" + titulo.toLowerCase() + "%"
            );
        };
    }
    
    public static Specification<Libro> tieneAutor(String autor) {
        return (root, query, criteriaBuilder) -> {
            if (autor == null || autor.trim().isEmpty()) {
                return criteriaBuilder.conjunction();
            }
            return criteriaBuilder.equal(root.get("autor"), autor);
        };
    }
    
    public static Specification<Libro> tieneGenero(String genero) {
        return (root, query, criteriaBuilder) -> {
            if (genero == null || genero.trim().isEmpty()) {
                return criteriaBuilder.conjunction();
            }
            return criteriaBuilder.equal(root.get("genero"), genero);
        };
    }
    
    public static Specification<Libro> precioEntre(BigDecimal precioMin, BigDecimal precioMax) {
        return (root, query, criteriaBuilder) -> {
            if (precioMin == null && precioMax == null) {
                return criteriaBuilder.conjunction();
            }
            if (precioMin != null && precioMax != null) {
                return criteriaBuilder.between(root.get("precio"), precioMin, precioMax);
            }
            if (precioMin != null) {
                return criteriaBuilder.greaterThanOrEqualTo(root.get("precio"), precioMin);
            }
            return criteriaBuilder.lessThanOrEqualTo(root.get("precio"), precioMax);
        };
    }
    
    public static Specification<Libro> estaDisponible() {
        return (root, query, criteriaBuilder) -> 
            criteriaBuilder.isTrue(root.get("disponible"));
    }
}

Creando un servicio para gestionar búsquedas

Implementamos un servicio que utilice nuestras specifications de forma práctica:

@Service
public class LibroService {
    
    private final LibroRepository libroRepository;
    
    public LibroService(LibroRepository libroRepository) {
        this.libroRepository = libroRepository;
    }
    
    public List<Libro> buscarLibros(String titulo, String autor, String genero, 
                                   BigDecimal precioMin, BigDecimal precioMax, 
                                   Boolean soloDisponibles) {
        
        Specification<Libro> spec = Specification.where(null);
        
        if (titulo != null && !titulo.trim().isEmpty()) {
            spec = spec.and(LibroSpecifications.tieneTitulo(titulo));
        }
        
        if (autor != null && !autor.trim().isEmpty()) {
            spec = spec.and(LibroSpecifications.tieneAutor(autor));
        }
        
        if (genero != null && !genero.trim().isEmpty()) {
            spec = spec.and(LibroSpecifications.tieneGenero(genero));
        }
        
        if (precioMin != null || precioMax != null) {
            spec = spec.and(LibroSpecifications.precioEntre(precioMin, precioMax));
        }
        
        if (soloDisponibles != null && soloDisponibles) {
            spec = spec.and(LibroSpecifications.estaDisponible());
        }
        
        return libroRepository.findAll(spec);
    }
    
    public long contarLibros(Specification<Libro> spec) {
        return libroRepository.count(spec);
    }
}

Implementando un controlador REST

Creamos un controlador que exponga nuestra funcionalidad de búsqueda:

@RestController
@RequestMapping("/api/libros")
public class LibroController {
    
    private final LibroService libroService;
    
    public LibroController(LibroService libroService) {
        this.libroService = libroService;
    }
    
    @GetMapping("/buscar")
    public ResponseEntity<List<Libro>> buscarLibros(
            @RequestParam(required = false) String titulo,
            @RequestParam(required = false) String autor,
            @RequestParam(required = false) String genero,
            @RequestParam(required = false) BigDecimal precioMin,
            @RequestParam(required = false) BigDecimal precioMax,
            @RequestParam(required = false) Boolean disponible) {
        
        List<Libro> libros = libroService.buscarLibros(
            titulo, autor, genero, precioMin, precioMax, disponible
        );
        
        return ResponseEntity.ok(libros);
    }
}

Specifications avanzadas con relaciones

Cuando trabajamos con entidades relacionadas, las Specifications también pueden navegar por estas relaciones. Supongamos que tenemos una entidad Editorial:

public class LibroSpecifications {
    
    public static Specification<Libro> tieneEditorial(String nombreEditorial) {
        return (root, query, criteriaBuilder) -> {
            if (nombreEditorial == null || nombreEditorial.trim().isEmpty()) {
                return criteriaBuilder.conjunction();
            }
            // Navegamos por la relación usando join
            Join<Libro, Editorial> editorialJoin = root.join("editorial");
            return criteriaBuilder.equal(editorialJoin.get("nombre"), nombreEditorial);
        };
    }
    
    public static Specification<Libro> publicadoDespuesDe(LocalDate fecha) {
        return (root, query, criteriaBuilder) -> {
            if (fecha == null) {
                return criteriaBuilder.conjunction();
            }
            return criteriaBuilder.greaterThan(root.get("fechaPublicacion"), fecha);
        };
    }
}

Uso práctico en el servicio

Veamos cómo utilizar estas specifications en combinaciones complejas:

@Service
public class LibroService {
    
    public List<Libro> buscarLibrosPopulares() {
        // Libros disponibles, publicados en los últimos 2 años, precio menor a 30€
        Specification<Libro> spec = Specification
            .where(LibroSpecifications.estaDisponible())
            .and(LibroSpecifications.publicadoDespuesDe(LocalDate.now().minusYears(2)))
            .and(LibroSpecifications.precioEntre(null, new BigDecimal("30.00")));
        
        return libroRepository.findAll(spec);
    }
    
    public List<Libro> buscarPorMultiplesGeneros(List<String> generos) {
        if (generos == null || generos.isEmpty()) {
            return Collections.emptyList();
        }
        
        // Crear specification que busque cualquiera de los géneros (OR)
        Specification<Libro> spec = null;
        for (String genero : generos) {
            Specification<Libro> generoSpec = LibroSpecifications.tieneGenero(genero);
            spec = (spec == null) ? generoSpec : spec.or(generoSpec);
        }
        
        return libroRepository.findAll(spec);
    }
}

Paginación con Specifications

Las Specifications se integran perfectamente con la paginación de Spring Data:

@GetMapping("/buscar-paginado")
public ResponseEntity<Page<Libro>> buscarLibrosPaginado(
        @RequestParam(required = false) String titulo,
        @RequestParam(required = false) String autor,
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "titulo") String sortBy) {
    
    Specification<Libro> spec = Specification.where(null);
    
    if (titulo != null && !titulo.trim().isEmpty()) {
        spec = spec.and(LibroSpecifications.tieneTitulo(titulo));
    }
    
    if (autor != null && !autor.trim().isEmpty()) {
        spec = spec.and(LibroSpecifications.tieneAutor(autor));
    }
    
    Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
    Page<Libro> libros = libroRepository.findAll(spec, pageable);
    
    return ResponseEntity.ok(libros);
}

Esta implementación nos proporciona un sistema de búsqueda flexible y escalable, donde podemos combinar múltiples criterios de filtrado sin necesidad de crear métodos específicos para cada combinación posible. Las Specifications mantienen nuestro código limpio, reutilizable y fácil de mantener.

Aprendizajes de esta lección

  • Comprender el patrón Specification y su utilidad para encapsular reglas de negocio en consultas dinámicas.
  • Aprender a implementar Specifications básicas y combinarlas para crear filtros complejos.
  • Integrar Specifications con repositorios que extienden JpaSpecificationExecutor en Spring Data JPA.
  • Aplicar Specifications en servicios y controladores REST para búsquedas flexibles y paginadas.
  • Manejar Specifications avanzadas que incluyen relaciones entre entidades y combinaciones lógicas complejas.

Completa SpringBoot y certifícate

Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración