JPA (EclipseLink) でのマッピングされていないデータベースカラムを含むクエリ

著者
Damian
Terlecki
4分間の読書
JPA

JPAの主な考え方はオブジェクトリレーショナルマッピングであり、 これにより、クエリを作成する際にデータベースのカラムを忘れて、マッピングされたオブジェクトのプロパティを扱うことができます。 しかし、マッピングされていないカラムを参照したい場合、 クエリの構築方法によっては、EclipseLinkのようなJPA固有の実装が提供するインターフェースを使用する必要があるかもしれません。

例えば、単純なusersテーブルを見てみましょう。

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

エンティティクラスの定義では、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;

}

さて、hiddenカラムに基づいてクエリを構築したいとしましょう。クエリの構築は次のように始めることができます。

  1. JPAのCriteriaBuilderインターフェースは、マッピングされたフィールドを介した参照を可能にします。カラムへの直接参照を構築するには、JPA実装のインターフェースを使用する必要があります。EclipseLinkの場合、手順は次のようになります。
  • CriteriaBuilderをJpaCriteriaBuilderにダウンキャストする。
  • エンティティ参照をEL固有のorg.eclipse.persistence.expressions.Expressionに変換し、getField()を介してカラム参照を作成し、それをJPA互換の形式に戻す。
  • 関連する条件を追加する。
  1. JPQL構文 – 上記と同様に、EL固有の構文を使用できます: WHERE SQL('hidden = true')
  2. ネイティブクエリ。
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);
    }

}

示されたすべてのケースで、EclipseLinkは期待される結果を返す正しいクエリを生成します。

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

他のJPA実装でも、同様のインターフェースが見つかるかもしれません。例えば、Hibernateは、同じ結果を達成するために使用できる条件構築中にRestrictions.sqlRestrictionメソッドを提供しています。