続・JPQLでハマった話

このブログについて

この記事は、Java EE Advent Calendar 2014 - Qiitaの21日目です。
昨日は@kokuzawaさんのRESTEasyとSpringの連携 - Katsumi Kokuzawa's Blogでした。
明日は@den2snさんです。

経緯および前回の記事

まずは、こちらをご覧ください。
JPQLでハマった話 - Java EE 事始め!
で、この前回の記事では「EclipseLinkではネイティブSQL使うしかない!」と結論づけてしまったのですが、改めてよく考えてみると気になる部分があったので、再調査しました。

気になった部分とは?

実行したときの例外ログです。

javax.persistence.PersistenceException: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: org.postgresql.util.PSQLException: ERROR: column "emp.ename" must appear in the GROUP BY clause or be used in an aggregate function

よーく見ると、PSQLExceptionが出ています。ということは、JPAレベルの問題ではなく、JDBCおよびSQLレベルの問題なのではないか、と思いました。

ログ出力の設定

persistence.xmlに、以下の記述を追加します。

EclipseLinkの場合

<property name="eclipselink.logging.level.sql" value="FINE" />
<property name="eclipselink.logging.parameters" value="true" />

Hibernateの場合

<property name="hibernate.show_sql" value="true" />

コード

EntityManagerFactory factory = Persistence.createEntityManagerFactory("empPU");
EntityManager manager = factory.createEntityManager();
String jpql = "SELECT SUBSTRING(e.ename, 1, 1), COUNT(e) FROM Emp e GROUP BY SUBSTRING(e.ename, 1, 1)";
List<Object[]> result = manager.createQuery(jpql, Object[].class)
        .getResultList();
result.forEach(array -> System.out.println(array[0] + " : " + array[1]));
manager.close();
factory.close();

実行されたSQL

EclipseLinkの場合

SELECT SUBSTR(ENAME, ?, ?), COUNT(EMPNO) FROM EMP GROUP BY SUBSTR(ENAME, ?, ?)
-> bind [1,1,1,1]

SQLはログに出ましたが、このSQLを実行した段階で例外になっているようです。

Hibernateの場合

select substring(emp0_.ename, 1, 1) as col_0_0_, count(emp0_.empno) as col_1_0_ 
from Emp emp0_ group by substring(emp0_.ename, 1, 1)

SQLが出力され、プログラムも実行できました。

違いは?

大きな違いは、「1」の部分がEclipseLinkはパラメータ化されているということです。
Hibernateはパラメータ化されていません。
これが原因ではないかと。

JDBCでやってみた

コード

String sql = "SELECT SUBSTR(ENAME, ?, ?), COUNT(EMPNO) FROM EMP GROUP BY SUBSTR(ENAME, ?, ?)";
Connection con = DriverManager.getConnection(url, user, password);
PreparedStatement pst = con.prepareStatement(sql);
pst.setInt(1, 1);
pst.setInt(2, 1);
pst.setInt(3, 1);
pst.setInt(4, 1);
ResultSet rs = pst.executeQuery();
while (rs.next()) {
    System.out.println(rs.getString(1) + " : " + rs.getInt(2));
}

実行結果

Exception in thread "main" org.postgresql.util.PSQLException: ERROR: column "emp.ename" must appear in the GROUP BY clause or be used in an aggregate function

EclipseLinkでやったときと同じ例外ですね。

推測ですが

おそらくパラメータ化されていると、SELECT句で指定された「SUBSTR(ENAME, ?, ?)」と、GROUP BY句で指定されている「SUBSTR(ENAME, ?, ?)」が同じものかどうかDBが判断できないため、例外になっているものと思われます。

EclipseLinkで実行する方法

つまり、EclipseLinkの場合、JPQLにベタ書きした定数がパラメータ化されてしまうため、例外が発生していたということになります。
この設定、persistence.xmlのプロパティでどうにか変更できるんでは?という仮説を思いつき、調べてみたら、ありました!
jdbc.bind-parameters | EclipseLink 2.5.x Java Persistence API (JPA) Extensions Reference
(「eclipselink properties persistence」でググりました)

<property name="eclipselink.jdbc.bind-parameters" value="false"/>

こうして最初のJavaプログラムを実行すると・・・

SELECT SUBSTR(ENAME, 1, 1), COUNT(EMPNO) FROM EMP GROUP BY SUBSTR(ENAME, 1, 1)
Y : 1
I : 2
K : 4
N : 3
S : 2
O : 1
A : 1

おおお、「1」の部分がパラメータ化されず、実行することができました!

この設定をしたとしても、通常のJPQLパラメータは使えますので、ご安心ください。

// 実行可能
String jpql = "SELECT e FROM Emp e WHERE e.empno = :empno";
Emp emp = manager.createQuery(jpql, Emp.class)
        .setParameter("empno", 102)  // パラメータ指定
        .getSingleResult();
System.out.println(emp.getEmpno() + " : " + emp.getEname());
manager.close();
factory.close();

まとめ

うーむ、JPA実装によって、こーゆー所も違うんですね。
後はやはり、SQLログは出しましょう、と。

eclipselink.jdbc.bind-parametersは、基本的にfalseを指定した方がよいかもしれません。
もし「この設定だとこういう場合まずいんじゃない?」といったことがありましたら、ぜひコメントいただければと思います。