Queries JPA (EclipseLink) com uma coluna de banco de dados não mapeada

Autor
Damian
Terlecki
6 minutos de leitura
JPA

A ideia principal por trás do JPA é o mapeamento objeto-relacional, graças ao qual podemos esquecer as colunas do banco de dados ao criar queries e trabalhar com propriedades de objetos mapeados. No entanto, se quisermos nos referir a uma coluna que não está mapeada, dependendo da forma como construímos a query, pode ser necessário usar uma interface fornecida pela implementação específica do JPA, como o EclipseLink.

Por exemplo, vamos pegar uma tabela de usuários simples:

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);

Na definição da classe da entidade, eu omito intencionalmente o mapeamento para a coluna 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;

}

Agora, suponha que queiramos construir uma query com base na coluna oculta. Podemos começar a construção da query com:

  1. A interface JPA CriteriaBuilder, que permite referência através de campos mapeados. Para construir uma referência direta à coluna, você precisa usar a interface de uma implementação JPA. No caso do EclipseLink, os passos seriam:
  • fazer um downcast do CriteriaBuilder para JpaCriteriaBuilder;
  • converter a referência da entidade para a org.eclipse.persistence.expressions.Expression específica do EL, criando a referência da coluna via getField() e convertendo-a de volta para a forma compatível com JPA;
  • adicionar a condição relevante;
  1. A sintaxe JPQL – como acima – podemos usar a sintaxe específica do EL: WHERE SQL('hidden = true');
  2. Uma query 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);
    }

}

Em todos os casos mostrados, o EclipseLink gera a query correta, retornando os 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 outras implementações de JPA, você pode encontrar interfaces semelhantes. Por exemplo, o Hibernate oferece os métodos Restrictions.sqlRestriction durante a construção dos critérios que podem ser usados para alcançar o mesmo resultado.