Consultas JPA (EclipseLink) con columnas no mapeadas
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:
- 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 congetField()
y convertirla de nuevo a una forma compatible con JPA; - añadir la condición relevante;
- Sintaxis JPQL – como arriba – podemos usar la sintaxis específica de EL:
WHERE SQL('hidden = true')
; - 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.