JPAエラー「java.lang.StringをLjava.lang.Stringにキャストできません」の修正
1. 前書き
もちろん、JavaでStringをString arrayにキャストできるとは思いもしませんでした。
java.lang.String cannot be cast to [Ljava.lang.String;
しかし、これは一般的なJPAエラーであることが判明しました。
このクイックチュートリアルでは、これがどのように発生し、どのように解決するかを示します。
2. JPAの一般的なエラーケース
JPAでは、このエラーが発生することは珍しくありませんwhen we work with native queries and we use the createNativeQuery method of the EntityManager.
そのJavadocは、実際にはthis method will return a list of Object[], or just an Object if only one column is returned by the query.であることを警告しています。
例を見てみましょう。 まず、すべてのクエリを実行するために再利用するクエリエグゼキュータを作成しましょう。
public class QueryExecutor {
public static List executeNativeQueryNoCastCheck(String statement, EntityManager em) {
Query query = em.createNativeQuery(statement);
return query.getResultList();
}
}
上記のように、createNativeQuery()メソッドを使用しており、常にString配列を含む結果セットを期待しています。
その後、例で使用する簡単なエンティティを作成しましょう。
@Entity
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String text;
// getters and setters
}
最後に、テストを実行する前にMessageを挿入するテストクラスを作成しましょう。
public class SpringCastUnitTest {
private static EntityManager em;
private static EntityManagerFactory emFactory;
@BeforeClass
public static void setup() {
emFactory = Persistence.createEntityManagerFactory("jpa-h2");
em = emFactory.createEntityManager();
// insert an object into the db
Message message = new Message();
message.setText("text");
EntityTransaction tr = em.getTransaction();
tr.begin();
em.persist(message);
tr.commit();
}
}
これで、QueryExecutorを使用して、エンティティのtextフィールドを取得するクエリを実行できます。
@Test(expected = ClassCastException.class)
public void givenExecutorNoCastCheck_whenQueryReturnsOneColumn_thenClassCastThrown() {
List results = QueryExecutor.executeNativeQueryNoCastCheck("select text from message", em);
// fails
for (String[] row : results) {
// do nothing
}
}
ご覧のとおり、because there is only one column in the query, JPA will actually return a list of strings, not a list of string arrays. クエリは単一の列を返し、配列を期待していたため、ClassCastExceptionを取得します。
3. 手動キャスト修正
ClassCastException.を回避するためのThe simplest way to fix this error is to check the type of the result set objectsQueryExecutorにそうするメソッドを実装しましょう:
public static List executeNativeQueryWithCastCheck(String statement, EntityManager em) {
Query query = em.createNativeQuery(statement);
List results = query.getResultList();
if (results.isEmpty()) {
return new ArrayList<>();
}
if (results.get(0) instanceof String) {
return ((List) results)
.stream()
.map(s -> new String[] { s })
.collect(Collectors.toList());
} else {
return (List) results;
}
}
次に、このメソッドを使用して、例外を取得せずにクエリを実行できます。
@Test
public void givenExecutorWithCastCheck_whenQueryReturnsOneColumn_thenNoClassCastThrown() {
List results = QueryExecutor.executeNativeQueryWithCastCheck("select text from message", em);
assertEquals("text", results.get(0)[0]);
}
クエリが1列のみを返す場合、結果を配列に変換する必要があるため、これは理想的なソリューションではありません。
4. JPAエンティティマッピングの修正
Another way to fix this error is by mapping the result set to an entity.このようにして、we can decide how to map the results of our queries in advanceを実行し、不要なキャストを回避します。
カスタムエンティティマッピングの使用をサポートするために、エグゼキュータに別のメソッドを追加しましょう。
public static List executeNativeQueryGeneric(String statement, String mapping, EntityManager em) {
Query query = em.createNativeQuery(statement, mapping);
return query.getResultList();
}
その後、カスタムSqlResultSetMapping を作成して、前のクエリの結果セットをMessageエンティティにマッピングしましょう。
@SqlResultSetMapping(
name="textQueryMapping",
classes={
@ConstructorResult(
targetClass=Message.class,
columns={
@ColumnResult(name="text")
}
)
}
)
@Entity
public class Message {
// ...
}
この場合、新しく作成したSqlResultSetMappingに一致するコンストラクターも追加する必要があります。
public class Message {
// ... fields and default constructor
public Message(String text) {
this.text = text;
}
// ... getters and setters
}
最後に、新しいエグゼキュータメソッドを使用してテストクエリを実行し、Messageのリストを取得できます。
@Test
public void givenExecutorGeneric_whenQueryReturnsOneColumn_thenNoClassCastThrown() {
List results = QueryExecutor.executeNativeQueryGeneric(
"select text from message", "textQueryMapping", em);
assertEquals("text", results.get(0).getText());
}
結果セットのマッピングをJPAに委任するため、このソリューションははるかにクリーンです。
5. 結論
この記事では、ネイティブクエリがこのClassCastExceptionを取得するための一般的な場所であることを示しました。 また、クエリの結果をトランスポートオブジェクトにマッピングして解決するだけでなく、自分で型チェックを行うことも検討しました。
いつものように、例の完全なソースコードはover on GitHubで入手できます。