JPA を使っていて、
必要としない関連エンティティにSQLが結構発行されるので、 カラム指定して必要なものだけ取るにはどうするか調べた結果をメモします。


参考サイト


発生したこと

Netbeans の エンティティ自動生成機能を使い、全てデフォルト設定でエンティティを生成したのですが、
ManyToOne の関連を持つテーブルにJPQLを実行した際、
以下のクエリが流れていました。

[EL Fine]: sql: 2016-10-30 12:53:14.109--ServerSession(315803384)--Connection(452259216)--SELECT id AS a1, create_date AS a2, description AS a3, name AS a4, site_url AS a5, update_date AS a6, held_year_id AS a7 FROM festival ORDER BY id ASC LIMIT ? OFFSET ?
    bind => [1000, 0]
[EL Fine]: sql: 2016-10-30 12:53:14.146--ServerSession(315803384)--Connection(452259216)--SELECT id, create_date, held_year, update_date FROM held_year WHERE (id = ?)
    bind => [6]
[EL Fine]: sql: 2016-10-30 12:53:14.156--ServerSession(315803384)--Connection(452259216)--SELECT id, create_date, held_year, update_date FROM held_year WHERE (id = ?)
    bind => [5]
[EL Fine]: sql: 2016-10-30 12:53:14.158--ServerSession(315803384)--Connection(452259216)--SELECT id, create_date, held_year, update_date FROM held_year WHERE (id = ?)
    bind => [4]
[EL Fine]: sql: 2016-10-30 12:53:14.159--ServerSession(315803384)--Connection(452259216)--SELECT id, create_date, held_year, update_date FROM held_year WHERE (id = ?)
    bind => [1]

実装上は、以下のJPQL発行のみ記述していますが、

SELECT id AS a1, create_date AS a2, description AS a3, name AS a4, site_url AS a5, update_date AS a6, held_year_id AS a7 FROM festival ORDER BY id ASC LIMIT ? OFFSET ?

ManyToOneの関係があるテーブルに対して、複数回、JQLが発行されていました。

SELECT id, create_date, held_year, update_date FROM held_year WHERE (id = ?)

テーブル構造は以下の通りです。
Festival N件に対し、HeldYear が 1件 対応する関係です。

Festival-HeldYear-Relation

この際に、複数回クエリが流れないようにしたい、そして、Fetchして関連エンティティを取得したいわけではない。
いうのが、今回やりたいことです。

Festivalから、HeldYearなしのデータを取得したいとも言えます。
上記クエリを発行している箇所の実装は以下の通りです。

    private static final java.lang.String FIND_ALL_ORDER_BY_ID = new StrBuilder()
            .appendln("SELECT ")
            .appendln("    f ")
            .appendln("FROM ")
            .appendln("    Festival f ")
            .appendln("ORDER BY f.id ASC")
            .build();

    /**
     * findAllByLimitAndOffset
     * @param limit
     * @param offest
     * @return
     */
    public List<Festival> findAllByLimitAndOffset(int limit, int offest) {
        TypedQuery<Festival> q = em.createQuery(FIND_ALL_ORDER_BY_ID, Festival.class);
        q.setFirstResult(offest);
        q.setMaxResults(limit);
        return q.getResultList();
    }


やってみたこと1(失敗事例)

NetBeansで Entityの生成時に、マッピングオプション 関連のフェッチを[デフォルト]から[遅延]に変更する。

NetBeansのデータベースからエンティティクラスの生成を行う際、
関連のフェッチが[デフォルト]の場合は、以下のようにアノテーションが生成されますが、

    @JoinColumn(name = "held_year_id", referencedColumnName = "id")
    @ManyToOne(optional = false)
    private HeldYear heldYearId;

関連のフェッチが[遅延]の場合は、以下のようにアノテーションが生成されます。

    @JoinColumn(name = "held_year_id", referencedColumnName = "id")
    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    private HeldYear heldYearId;

この状態で再デプロイして挙動を確認してみましたが、
相変わらず、クエリは実行されていました。


やってみたこと2(失敗事例)

JPQL を 全Column取得から、必要なデータのみ取得に切り替える。

JPQLをエンティティ指定での取得から、
カラム指定で取得するように修正しました。

  • 修正後

    private static final java.lang.String FIND_ALL_ORDER_BY_ID = new StrBuilder()
            .appendln("SELECT ")
            .appendln("    f.id, ")
            .appendln("    f.name, ")
            .appendln("    f.siteUrl, ")
            .appendln("    f.description, ")
            .appendln("    f.createDate, ")
            .appendln("    f.updateDate ")
            .appendln("FROM ")
            .appendln("    Festival f ")
            .appendln("ORDER BY f.id ASC")
            .build();

こちらを実行したところ、
java.lang.ClassCastException が発生。

java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to xyz.monotalk.festivals4partypeople.models.rdb.entity.Festival

クエリで取得しているのが、カラム値なので、EntityクラスにCastできずに落ちる。
いうことになりました。


やってみたこと3(失敗事例)

2 の JPQL を javax.persistence.Tuple に マッピング

以下のようにjavax.persistence.Tuple にマッピングして、 実行したところ、
java.lang.ClassCastException が発生しました。
まあ、Object型って言ってるから、それは変換できませんよですね。。

java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to javax.persistence.Tuple

    /**
     * findAllByLimitAndOffset
     * @param limit
     * @param offest
     * @return
     */
    public List<javax.persistence.Tuple> findAllByLimitAndOffset(int limit, int offest) {
        TypedQuery<javax.persistence.Tuple> q = em.createQuery(FIND_ALL_ORDER_BY_ID, javax.persistence.Tuple.class);
        q.setFirstResult(offest);
        q.setMaxResults(limit);
        return q.getResultList();
    }


やってみたこと4(成功事例)

2 の JPQL を Object[] に マッピング

以下の実装で、ようやく結果が取得できるようになりました。 が、可読性が悪い、且つ、何より、使いづらい。。

    /**
     * findAllByLimitAndOffset
     * @param limit
     * @param offest
     * @return
     */
    public List<Object[]> findAllByLimitAndOffset(int limit, int offest) {
        TypedQuery<Object[]> q = em.createQuery(FIND_ALL_ORDER_BY_ID, Object[].class);
        q.setFirstResult(offest);
        q.setMaxResults(limit);
        return q.getResultList();
    }


やってみたこと5(成功事例)

new キーワード を 使って JPQL を Entityクラス に マッピング

JPQLと、実装を以下のように書き換えます。 やっといい感じに取得できるようになりました。
関連エンティティは取得しにいってないです。且つ、エンティティで取得できるようになってます。

  • JPQL

    private static final java.lang.String FIND_ALL_ORDER_BY_ID = new StrBuilder()
            .appendln("SELECT NEW ")
            .appendln("xyz.monotalk.festivals4partypeople.models.rdb.entity.Festival (")
            .appendln("    f.id, ")
            .appendln("    f.name, ")
            .appendln("    f.siteUrl, ")
            .appendln("    f.description, ")
            .appendln("    f.createDate, ")
            .appendln("    f.updateDate ")
            .appendln(") ")
            .appendln("FROM ")
            .appendln("    Festival f ")
            .appendln("ORDER BY f.id ASC")
            .build();

  • java

    /**
     * findAllByLimitAndOffset
     *
     * @param limit
     * @param offest
     * @return
     */
    public List<Festival> findAllByLimitAndOffset(int limit, int offest) {
        TypedQuery<Festival> q = em.createQuery(FIND_ALL_ORDER_BY_ID, Festival.class);
        q.setFirstResult(offest);
        q.setMaxResults(limit);
        return q.getResultList();
    }

やってみたこと6(成功事例)

Querydsl を使う

Querydsl という、JPA でも使える QueryBuilder ライブラリがあったので、
それを使って書いてみます。

  • 事前準備 pom.xml に 依存関係を追加

最新Version が 4.1.4 なのでそれを、pom.xml に追加します。
jpa で使うには、querydsl-core、querydsl-jpa、querydsl-sql の 3つが必要でした。

    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-core</artifactId>
        <version>4.1.4</version>
    </dependency>
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
        <version>4.1.4</version>
    </dependency>
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-sql</artifactId>
        <version>4.1.4</version>
    </dependency>

  • 事前準備 pom.xml に アノテーションプロセッサの定義を追加

Entity の カラム定義クラス? QEntity を生成するための、
アノテーションプロセッサ の 定義を追加します。

    <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>apt-maven-plugin</artifactId>
        <version>1.1.3</version>
        <executions>
            <execution>
                <goals>
                    <goal>process</goal>
                </goals>
                <configuration>
                    <outputDirectory>target/generated-sources/java</outputDirectory>
                    <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                </configuration>
            </execution>
        </executions>
        <dependencies>
            <dependency>
                <groupId>com.querydsl</groupId>
                <artifactId>querydsl-apt</artifactId>
                <version>4.1.4</version>
            </dependency>
        </dependencies>
    </plugin>

以下、JPQLを Querydsl で書き換えた実装です。
Projections#constructor()で、Festival.class を指定することで、
HeldYear を 取得することなく、Festival に 結果を設定することができました。

  • java

    /**
     * findAllByLimitAndOffset
     *
     * @param limit
     * @param offest
     * @return
     */
    public List<Festival> findAllByLimitAndOffset(int limit, int offest) {
        QFestival qFestival = QFestival.festival;
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        List<Festival> results = queryFactory
                .select(Projections.constructor(
                        Festival.class,
                        qFestival.id,
                        qFestival.name,
                        qFestival.description,
                        qFestival.siteUrl,
                        qFestival.updateDate,
                        qFestival.createDate))
                .from(qFestival)
                .orderBy(qFestival.id.asc())
                .limit(limit)
                .offset(offest).fetch();
        return results;
    }

個人的には、Querydsl で書くようにしようかと。
以上です。

コメント