ContractS開発者ブログ

契約マネジメントシステム「ContractS CLM」の開発者ブログです。株式会社HolmesはContractS株式会社に社名変更しました。

(Java)QueryDslを使用して重複除去(DISTINCT)とソート(ORDER BY)を両立させる

ContractSの倉島です。
最近、DBへのアクセスを型安全にすべくQueryDslを用いているのですが困ったことに直面したので備忘録的に解決手段を記しておきます。

何に困ったか

とあるデータを一覧で取得する際に、「レコードの重複除去を行いつつ取得していない値でソートを行う」必要が出てきました。
しかし、ただ単純にDISTINCTとORDER BYを同時に指定するだけでは順番が狂って取得されてしまいました。

何が原因か

DISTINCTは内部でソートしてから重複除去を行うらしく同時に指定した場合は、DISTINCTが実行された後にORDER BYが実行されてしまうので狂った並び順でソートされてしまうと原因づけました。

解決策

MySQLのクエリだったら、親のテーブルの重複除去を行った結果を副問い合わせとしてそれをソート対象の値があるテーブルと結合すれば、DISTINCTとORDER BYは別々で実行されるので正しく取得できるかと思います。
ただ、QueryDslには副問い合わせの結果をjoinする機能は存在しない(もし存在していたらすみません!)ので、この解決策は使えません。
そこで考えたのが、ORDER BYに指定する値を副問い合わせ結果にしてしまうという策です。

具体的には

修正前(これだと狂った並び順で取得される)

              queryFactory
                .select(Projections.constructor(
                        SearchedResultEntity.class,
                        qMainEntity.mainId,
                        qMainEntity.xxx,
                        qSubEntity.subId,
                        qSubEntity.yyy
                ))
                .distinct()
                .from(qMainEntity)
                .innerJoin(qSubEntity)
                .on(qSubEntity.mainId.eq(qMainEntity.mainId))
                .leftJoin(qSubSubEntity)
                .on(qSubSubEntity.subId.eq(qSubEntity.subId))
                .orderBy(qSubSubEntity.property.asc());

修正後(これで正しい順番で取得される)

              SubQueryExpression<Long> subQuery =
                JPAExpressions.select(qSubSubEntity.value).from(qSubSubEntity)
                        .where(qSubSubEntity.propertyId.eq('sortPropertyId'),
                                qSubSubEntity.subId.eq(qContractHistoryEntity.subId));

              OrderSpecifier<Long> sort = Expressions.asNumber(subQuery).asc()

              queryFactory
                .select(Projections.constructor(
                        SearchedResultEntity.class,
                        qMainEntity.mainId,
                        qMainEntity.xxx,
                        qSubEntity.subId,
                        qSubEntity.yyy
                ))
                .distinct()
                .from(qMainEntity)
                .innerJoin(qSubEntity)
                .on(qSubEntity.mainId.eq(qMainEntity.mainId))
                .leftJoin(qSubSubEntity)
                .on(qSubSubEntity.subId.eq(qSubEntity.subId))
                .orderBy(sort);

取得する際はfetch()の実行もお忘れなく。
また、副問い合わせを使用する関係上、扱うデータが大量だとパフォーマンスに対する懸念が生まれるのでご注意ください。

最後に

クエリ文を直接記述する方法なら、最初の「親テーブルを重複除去した結果」を結合してしまえば解決していたことを考えると、今回の問題に限って言えばQueryDslは少し不便かもしれないですね。
しかしながらそれを補って余りある型安全やメソッドチェーンによる可読性の向上があると考えているので今後もQueryDslを有効活用できればと思います。