JPAエラー「java.lang.Stringを[Ljava.lang.String;」にキャストできません. 」の修正

JPAエラー「java.lang.StringをLjava.lang.Stringにキャストできません」の修正

1. 前書き

もちろん、JavaでStringString 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で入手できます。