続・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]
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();