Ejemplos de consultas usando JPA Criteria API

10/06/2016

jpa-mini-logo

Introducción

Supongamos que tenemos las siguientes tablas con sus correspondientes relaciones:

tablas

Que tendríamos mapeadas en las siguientes entidades JPA:
TipoRecurso.java

@Entity
@Table(name = "tipo_recurso")
public class TipoRecurso implements Serializable {
 
    @Id
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;
 
    @Column(name = "nombre", length = 150)
    private String nombre;
 
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
 
    public String getNombre() {
        return nombre;
    }
    public void setNombre(String nombre) {
        this.nombre = nombre;
    }
}

Recurso.java

@Entity
@Table(name = "recurso")
public class Recurso implements Serializable {
 
    @Id
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;
 
    @Column(name = "titulo", length = 255)
    private String titulo;
 
    @ManyToOne
    @JoinColumn(name = "id_tipo_recurso")
    private TipoRecurso tipoRecurso;
 
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
 
    public String getTitulo() {
        return titulo;
    }
    public void setTitulo(String titulo) {
        this.titulo = titulo;
    }
 
    public TipoRecurso getTipoRecurso() {
        return tipoRecurso;
    }
    public void setTipoRecurso(TipoRecurso tipoRecurso) {
        this.tipoRecurso = tipoRecurso;
    }
}

Tematica.java

@Entity
@Table(name = "tematica")
public class Tematica implements Serializable {
 
    @Id
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;
 
    @Column(name = "nombre", length = 150)
    private String nombre;
 
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
 
    public String getNombre() {
        return nombre;
    }
    public void setNombre(String nombre) {
        this.nombre = nombre;
    }
}

RecursoTematica.java

@Entity
@Table(name = "recurso_tematica")
public class RecursoTematica implements Serializable {
 
    @Id
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;
 
    @ManyToOne
    @JoinColumn(name = "id_recurso", referencedColumnName="id")
    private Recurso recurso;
 
    @ManyToOne
    @JoinColumn(name = "id_tematica", referencedColumnName="id")
    private Tematica tematica;
 
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
 
    public Recurso getRecurso() {
        return recurso;
    }
    public void setRecurso(Recurso recurso) {
        this.recurso = recurso;
    }
 
    public Tematica getTematica() {
        return tematica;
    }
    public void setTematica(Tematica tematica) {
        this.tematica = tematica;
    }
}

Dados estos componentes vamos a ver cómo podríamos construir una búsqueda personalizada que nos permita aplicar los valores de filtrado especificados en la siguiente clase:
RecursoFilter.java

public class RecursoFilter implements Serializable {
    private String titulo;
    private TipoRecurso tipoRecurso;
    private List idTematicas;
 
    public String getTitulo() {
        return titulo;
    }
    public void setTitulo(String titulo) {
        this.titulo = titulo;
    }
 
    public TipoRecurso getTipoRecurso() {
        return tipoRecurso;
    }
    public void setTipoRecurso(TipoRecurso tipoRecurso) {
        this.tipoRecurso = tipoRecurso;
    }
 
    public List getIdTematicas() {
        return idTematicas;
    }
    public void setIdTematicas(List idTematicas) {
        this.idTematicas = idTematicas;
    }
}

Ya que lo que queremos recuperar son objetos de tipo Recurso, siguiendo con la propuesta del uso de Specification de Spring Data, crearíamos un nuevo método para la generación de la especificación que realice el filtrado en la clase RecursoSpecificactions:
RecursoSpecifications.java

public final class RecursoSpecifications {
    public static Specification likeFilter(
            final RecursoFilter filter) {
        Specification returnSpecification = new Specification() {
            @Override
            public Predicate toPredicate(Root root,
                    CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                ....
            }
        };
        return returnSpecification;
    }
    private RecursoSpecifications() {
        // this prevents even the native class from
        // calling this constructor as well
        throw new AssertionError();
    }
}

De este modo Spring Data nos proporcionará “de serie” el soporte para la paginación y ordenación de los resultados.

Restricción simple

Comencemos por ver cómo sería aplicar un filtrado sobre el campo titulo de la entidad Recurso, usando como patrón de filtrado un like con el contenido del campo titulo del RecursoFilter:
RecursoSpecifications.java

...
public Predicate toPredicate(Root root,
        CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
    List predicates = new ArrayList();
    // Restricción simple
    if (StringUtils.isNotEmpty(filter.getTitulo())) {
        Predicate likeTitulo = criteriaBuilder.like(
                criteriaBuilder.lower(root. get(Recurso_.titulo)),
                "%" + filter.getTitulo().toLowerCase() + "%");
        predicates.add(likeTitulo);
    }
    Predicate restrictions = criteriaBuilder
            .and(predicates.toArray(new Predicate[] {}));
return restrictions;
}
...

Recurso_ es la clase de MetaData JPA generada automáticamente a partir de la entidad Recurso.

El código anterior generará una SQL similar a:

select
    recurso0_.id as id1_0_,
    recurso0_.id_tipo_recurso as id_tipo_3_0_,
    recurso0_.titulo as titulo2_0_
from
    recurso recurso0_
where
    lower(recurso0_.titulo) like ?

Restricción con entidad mapeada

Veamos ahora cómo añadir la restricción de búsqueda por tipo de recurso:
RecursoSpecifications.java

...
public Predicate toPredicate(Root root,
        CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
    List predicates = new ArrayList();
    ...
    // Restricción con entidad mapeada
    if (filter.getTipoRecurso() != null) {
        // Con JOIN de entidad
        Join<Recurso, TipoRecurso> join = root.join(Recurso_.tipoRecurso);
        Predicate equalsTipoRecurso = criteriaBuilder.equal(join, filter.getTipoRecurso());
        predicates.add(equalsTipoRecurso);
    }
    Predicate restrictions = criteriaBuilder
            .and(predicates.toArray(new Predicate[] {}));
return restrictions;
}
...

Esta restricción nos permitiría realizar la búsqueda tanto por id te TipoRecurso como por nombre de TipoRecurso.

El código anterior generará una SQL similar a:

select
    recurso0_.id as id1_0_,
    recurso0_.id_tipo_recurso as id_tipo_3_0_,
    recurso0_.titulo as titulo2_0_
from
    recurso recurso0_
inner join
    tipo_recurso tiporecurs1_
        on recurso0_.id_tipo_recurso=tiporecurs1_.id
where
    tiporecurs1_.id=?

Restricción (optimizada) con entidad mapeada

Si sabemos que siempre vamos a hacer la búsqueda usando el id de TipoRecurso, nos podemos evitar el cruce con la tabla tipo_recurso si modificamos el código anterior por el siguiente:
RecursoSpecifications.java

...
public Predicate toPredicate(Root root,
        CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
    List predicates = new ArrayList();
    ...
    // Restricción con entidad mapeada
    if (filter.getTipoRecurso() != null) {
        // Más optimo, igualando id's (evita el join)
        Predicate equalTipo = criteriaBuilder.equal(
                root.get(Recurso_.tipoRecurso)
                        .get(TipoRecurso_.id),
                filter.getTipoRecurso().getId());
        predicates.add(equalTipo);
    }
    Predicate restrictions = criteriaBuilder
            .and(predicates.toArray(new Predicate[] {}));
return restrictions;
}
...

El código anterior generará una SQL similar a:

select
    recurso0_.id as id1_0_,
    recurso0_.id_tipo_recurso as id_tipo_3_0_,
    recurso0_.titulo as titulo2_0_
from
    recurso recurso0_
where
    recurso0_.id_tipo_recurso=1

Restricción con entidad no mapeada

Vamos ahora a añadir un poco más de complejidad a la consulta, añadiendo la posibilidad de filtrar por Tematica; entidad con la que Recurso no está “mapeada” directamente. Concretamente vamos a filtrar para incluir todos los recursos que se correspondan con alguno de los id de Tematica contenidos en una lista:
RecursoSpecifications.java

...
public Predicate toPredicate(Root root,
        CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
    List predicates = new ArrayList();
    ...
    // Restricción con entidad no mapeada en la principal
    if (filter.getIdTematicas() != null) {
        Root recursoTematicaRoot = criteriaQuery
                .from(RecursoTematica.class);
        // Join de las tablas por el campo por el que "deberían" estar
        // mapeadas
        Predicate joinRecursoRecursoTematica = criteriaBuilder.equal(
                root.get(Recurso_.id),
                recursoTematicaRoot.get(RecursoTematica_.recurso)
                        .get(Recurso_.id));
        // Restricciones a aplicar sobre la segunda entidad
        Predicate idTematicasIn = recursoTematicaRoot
                .get(RecursoTematica_.tematica).get(Tematica_.id)
                .in(filter.getIdTematicas());
        // Juntamos todas las restricciones
        Predicate tematicasRelation = criteriaBuilder
                .and(joinRecursoRecursoTematica, idTematicasIn);
        predicates.add(tematicasRelation);
    }
    Predicate restrictions = criteriaBuilder
            .and(predicates.toArray(new Predicate[] {}));
return restrictions;
}
...

El código anterior generará una SQL similar a:

select
    recurso0_.id as id1_0_,
    recurso0_.id_tipo_recurso as id_tipo_3_0_,
    recurso0_.titulo as titulo2_0_
from
    recurso recurso0_ cross
join
    recurso_tematica recursotem1_
where
    recurso0_.id=recursotem1_.id_recurso
    and (
        recursotem1_.id_tematica in (
            1 , 2
        )
    )

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: