Zapytania JPA (EclipseLink) zawierające niezmapowaną kolumnę

Autor
Damian
Terlecki
6 minut
JPA

Główną ideą stojącą za interfejsem JPA jest mapowanie obiektowo relacyjne, dzięki któremu podczas konstrukcji zapytań zapominamy o kolumnach bazodanowych i operujemy na zmapowanych właściwościach obiektu. Jeśli jednak chcemy odnieść się do kolumny, której nie odwzorowaliśmy, to w zależności od tego, jak budujemy zapytanie, jego zrealizowanie może wymagać skorzystania z interfejsu dostarczanego przez implementację JPA, np. EclipseLinka.

Na tapet weźmy prostą tabelę użytkowników:

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

W definicji klasy encji celowo pominę mapowanie dla kolumny 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;

}

Zakładając teraz, że chcemy skonstruować zapytanie w oparciu o kolumnę hidden, możemy wystartować od:

  1. Interfejsu JPA CriteriaBuilder – pozwala na referencję poprzez zmapowane pola, a w celu skonstruowania bezpośredniego odniesienia do kolumny konieczne będzie skorzystanie z interfejsu implementacji. W przypadku EclipseLink będzie to:
  • zrzutowanie klasy CriteriaBuilder na JpaCriteriaBuilder;
  • przekształcenie referencji do encji na interfejs org.eclipse.persistence.expressions.Expression, utworzenie odniesienie się do kolumy poprzez getField() i przekształcenie z powrotem do formy kompatybilnej z JPA;
  • standardowe dodanie warunku;
  1. Składni JPQL – podobnie jak wyżej – możemy wykorzystać składnię specyficzną dla EL: WHERE SQL('hidden = true');
  2. Utworzenie zapytania w formie natywnej.
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);
    }

}

We wszystkich pokazanych przypadkach EclipseLink wygeneruje właściwe zapytanie i otrzymamy oczekiwane rezultaty:

[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)]

W przypadku innych implementacji JPA możemy znaleźć podobne interfejsy. Przykładowo Hibernate oferuje metodę Restrictions.sqlRestriction podczas konstrukcji kryteriów do uzyskania takiego samego rezultatu.