Consultas JPA (EclipseLink) con columnas no mapeadas

Autor
Damian
Terlecki
6 minutos de lectura
JPA

La idea principal detrás de JPA es el mapeo objeto-relacional, gracias al cual podemos olvidarnos de las columnas de la base de datos al crear consultas y trabajar con propiedades mapeadas del objeto. Sin embargo, si queremos referirnos a una columna que no está mapeada, dependiendo de cómo construyamos la consulta, puede ser necesario usar una interfaz específica de la implementación JPA, como EclipseLink.

Por ejemplo, tomemos una tabla de usuarios sencilla:

CREATE TABLE users
(
    id     BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
    hidden BOOLEAN DEFAULT false,
    name   VARCHAR(255),
    PRIMARY KEY (id)
);
INSERT INTO users (name, hidden) VALUES ('Adam', true);
INSERT INTO users (name, hidden) VALUES ('Damian', false);
INSERT INTO users (name, hidden) VALUES ('Emma', true);
INSERT INTO users (name, hidden) VALUES ('Alice', false);

En la definición de la clase entidad, omito intencionadamente el mapeo para la columna hidden:

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Getter
@Setter
@ToString(exclude = "id")
@Entity
@Table(name="users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

}

Ahora supongamos que queremos construir una consulta basada en la columna hidden. Podemos empezar la construcción de la consulta con:

  1. La interfaz CriteriaBuilder de JPA, que permite referenciar solo campos mapeados. Para referenciar directamente la columna, necesitas usar la interfaz de una implementación JPA. En el caso de EclipseLink, los pasos serían:
  • hacer downcast de CriteriaBuilder a JpaCriteriaBuilder;
  • convertir la referencia de entidad a org.eclipse.persistence.expressions.Expression específica de EL, crear la referencia de columna con getField() y convertirla de nuevo a una forma compatible con JPA;
  • añadir la condición relevante;
  1. Sintaxis JPQL – como arriba – podemos usar la sintaxis específica de EL: WHERE SQL('hidden = true');
  2. Una consulta nativa.
import org.eclipse.persistence.jpa.JpaCriteriaBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import java.util.List;

@SpringBootTest
class DemoApplicationTests {

    @PersistenceContext
    EntityManager em;

    @Test
    void testUnmappedFieldCriteria() {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<User> criteria = builder.createQuery(User.class);
        List<User> users = em.createQuery(criteria).getResultList();
        System.out.println("All users: " + users);

        // #1
        JpaCriteriaBuilder jpaBuilder = (JpaCriteriaBuilder) builder;
        Expression<Boolean> hiddenField = jpaBuilder.fromExpression(
                jpaBuilder.toExpression(criteria.from(User.class)).getField("hidden"),
                Boolean.class
        );
        users = em.createQuery(criteria.where(builder.equal(hiddenField, false)))
                .getResultList();
        System.out.println("#1 Visible users using JpaCriteriaBuilder: " + users);

        // #2
        users = em.createQuery("SELECT u FROM User u WHERE SQL('hidden = true')", User.class)
                .getResultList();
        System.out.println("#2 Hidden users using JPQL (EL-flavored): " + users);

        // #3
        users = em.createNativeQuery("SELECT * FROM users WHERE hidden = true", User.class)
                .getResultList();
        System.out.println("#3 Hidden users using native query: " + users);
    }

}

En todos los casos mostrados, EclipseLink genera la consulta correcta devolviendo los resultados esperados:

[EL Fine]: sql: 2022-01-09 13:51:28.215--ServerSession(2027837674)--Connection(1139915666)--Thread(Thread[main,5,main])--SELECT ID, NAME FROM users
All users: [User(name=Adam), User(name=Damian), User(name=Emma), User(name=Alice)]

[EL Fine]: sql: 2022-01-09 13:51:28.258--ServerSession(2027837674)--Connection(1139915666)--Thread(Thread[main,5,main])--SELECT ID, NAME FROM users WHERE (hidden = ?)
    bind => [false]
#1 Visible users using JpaCriteriaBuilder: [User(name=Damian), User(name=Alice)]

[EL Fine]: sql: 2022-01-09 13:51:28.616--ServerSession(2027837674)--Connection(1139915666)--Thread(Thread[main,5,main])--SELECT ID, NAME FROM users WHERE hidden = true
#2 Hidden users using JPQL (EL-flavored): [User(name=Adam), User(name=Emma)]

[EL Fine]: sql: 2022-01-09 13:51:28.634--ServerSession(2027837674)--Connection(1139915666)--Thread(Thread[main,5,main])--SELECT * FROM users WHERE hidden = true
#3 Hidden users using native query: [User(name=Adam), User(name=Emma)]

Para otras implementaciones JPA, puedes encontrar interfaces similares. Por ejemplo, Hibernate ofrece los métodos Restrictions.sqlRestriction durante la construcción de criterios para lograr el mismo resultado.