Рубрики
Без рубрики

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

Узнайте, как исправить распространенную ошибку приведения в запросах JPA.

Автор оригинала: Marcos Lopez Gonzalez.

1. введение

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

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

Но, оказывается, это распространенная ошибка JPA.

В этом кратком уроке мы покажем, как это происходит и как это решить.

2. Распространенный случай ошибки в JPA

В JPA нередко возникает эта ошибка , когда мы работаем с собственными запросами и используем метод createNativeQuery в EntityManager .

Его Javadoc фактически предупреждает нас , что этот метод вернет список Object [] или просто Object , если запрос возвращает только один столбец.

Давайте рассмотрим пример. Во-первых, давайте создадим исполнителя запросов, который мы хотим повторно использовать для выполнения всех наших запросов:

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

}

И, наконец, давайте создадим тестовый класс, который вставляет Сообщение перед запуском тестов:

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

Теперь мы можем использовать наш Исполнитель запросов для выполнения запроса, который извлекает текстовое поле нашей сущности:

@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
    }
}

Как мы видим, поскольку в запросе есть только один столбец, JPA фактически вернет список строк, а не список строковых массивов. Мы получаем ClassCastException , потому что запрос возвращает один столбец, и мы ожидали массив.

3. Ручная Фиксация Литья

Самый простой способ исправить эту ошибку-проверить тип объектов результирующего набора , чтобы избежать исключения ClassCastException. Давайте реализуем метод для этого в нашем Исполнителе запросов :

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

Другой способ исправить эту ошибку-сопоставить результирующий набор с сущностью. Таким образом, мы можем заранее решить, как сопоставить результаты наших запросов и избежать ненужных отливок.

Давайте добавим еще один метод к нашему исполнителю для поддержки использования пользовательских сопоставлений сущностей:

public static  List executeNativeQueryGeneric(String statement, String mapping, EntityManager em) {
    Query query = em.createNativeQuery(statement, mapping);
    return query.getResultList();
}

После этого давайте создадим пользовательское SqlResultSetMapping для сопоставления результирующего набора нашего предыдущего запроса с Сообщением сущностью:

@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

}

Наконец, мы можем использовать наш новый метод исполнителя для выполнения тестового запроса и получения списка Сообщений :

@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 . Мы также рассмотрели возможность выполнения проверки типа самостоятельно, а также ее решения путем сопоставления результатов запроса с транспортным объектом.

Как всегда, полный исходный код примеров доступен на GitHub .