Исправление ошибки JPA «java.lang.String не может быть приведен к[Ljava.lang.String;»

Исправление ошибки JPA «java.lang.String не может быть приведен к Ljava.lang.String;»

1. Вступление

Конечно, мы никогда не предполагали, что можем преобразоватьString в массивString в Java:

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. Исправление ручного литья

The simplest way to fix this error is to check the type of the result set objects, чтобы избежатьClassCastException.. Давайте реализуем метод для этого в нашемQueryExecutor:

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]);
}

Это не идеальное решение, поскольку мы должны преобразовать результат в массив, если запрос возвращает только один столбец.

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 to, отображающий набор результатов нашего предыдущего запроса на объект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.