1前書き

もちろん、Javaで

String



__String

__配列にキャストできるとは限りません。

java.lang.String cannot be cast to[Ljava.lang.String;

しかし、これは一般的なJPAエラーです。

このクイックチュートリアルでは、これがどのように発生するのか、およびそれを解決する方法を説明します。


2 JPA

における一般的なエラーケース

JPAでは、ネイティブクエリを処理するときにこのエラーが発生することは珍しくありません。また、

EntityManager



createNativeQuery

メソッドを使用します。

そのhttps://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html#createNativeQuery-java.lang.String-[Javadoc]は、実際には** このメソッドは

Objectのリストを返すことを警告しています[]

、またはクエリによって返される列が1つだけの場合は

Object

だけです。

例を見てみましょう。まず、すべてのクエリを実行するために再利用したいクエリエグゼキュータを作成しましょう。

public class QueryExecutor {
    public static List<String[]> 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<String[]> results = QueryExecutor.executeNativeQueryNoCastCheck("select text from message", em);

   //fails
    for (String[]row : results) {
       //do nothing
    }
}

ご覧のとおり、


クエリには列が1つしかないため、JPAは実際には文字列配列のリストではなく、文字列のリストを返します。


クエリが単一の列を返し、配列が必要なため、

ClassCastException

が発生します。

3.手動キャスティング修正

  • このエラーを修正する最も簡単な方法は、

    ClassCastExceptionを回避するために、結果セットオブジェクトの型をチェックすることです。

    そうするためのメソッドを

    QueryExecutor

    に実装しましょう。

public static List<String[]> 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<String>) results)
          .stream()
          .map(s -> new String[]{ s })
          .collect(Collectors.toList());
    } else {
        return (List<String[]>) results;
    }
}

それから、このメソッドを使用して、例外を受け取ることなくクエリを実行できます。

@Test
public void givenExecutorWithCastCheck__whenQueryReturnsOneColumn__thenNoClassCastThrown() {
    List<String[]> results = QueryExecutor.executeNativeQueryWithCastCheck("select text from message", em);
    assertEquals("text", results.get(0)[0]);
}

クエリが1列しか返さない場合は、結果を配列に変換する必要があるため、これは理想的な解決策ではありません。

4. JPAエンティティマッピングの修正

このエラーを修正するもう1つの方法は、結果セットをエンティティにマッピングすることです。

カスタムエンティティマッピングの使用をサポートするために別のメソッドをexecutorに追加しましょう。

public static <T> List<T> 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

}

最後に、新しいexecutorメソッドを使ってテストクエリを実行し、

Message

のリストを取得します。

@Test
public void givenExecutorGeneric__whenQueryReturnsOneColumn__thenNoClassCastThrown() {
    List<Message> results = QueryExecutor.executeNativeQueryGeneric(
      "select text from message", "textQueryMapping", em);
    assertEquals("text", results.get(0).getText());
}

このソリューションは、結果セットマッピングをJPAに委任しているため、はるかにクリーンです。


5結論

この記事では、ネイティブクエリがこの

ClassCastException

を取得する一般的な場所であることを示しました。また、クエリ結果をトランスポートオブジェクトにマッピングすることで解決するだけでなく、自分で型チェックを行うことも検討しました。

いつものように、例の完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/persistence-modules/java-jpa[GitHubで利用可能]です。