Автор оригинала: 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 ListexecuteNativeQueryNoCastCheck(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() { Listresults = QueryExecutor.executeNativeQueryNoCastCheck("select text from message", em); // fails for (String[] row : results) { // do nothing } }
Как мы видим, поскольку в запросе есть только один столбец, JPA фактически вернет список строк, а не список строковых массивов. Мы получаем ClassCastException , потому что запрос возвращает один столбец, и мы ожидали массив.
3. Ручная Фиксация Литья
Самый простой способ исправить эту ошибку-проверить тип объектов результирующего набора , чтобы избежать исключения ClassCastException. Давайте реализуем метод для этого в нашем Исполнителе запросов :
public static ListexecuteNativeQueryWithCastCheck(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() { Listresults = QueryExecutor.executeNativeQueryWithCastCheck("select text from message", em); assertEquals("text", results.get(0)[0]); }
Это не идеальное решение, так как мы должны преобразовать результат в массив, если запрос возвращает только один столбец.
4. Исправление сопоставления сущностей JPA
Другой способ исправить эту ошибку-сопоставить результирующий набор с сущностью. Таким образом, мы можем заранее решить, как сопоставить результаты наших запросов и избежать ненужных отливок.
Давайте добавим еще один метод к нашему исполнителю для поддержки использования пользовательских сопоставлений сущностей:
public staticList 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() { Listresults = QueryExecutor.executeNativeQueryGeneric( "select text from message", "textQueryMapping", em); assertEquals("text", results.get(0).getText()); }
Это решение намного чище, так как мы делегируем отображение результирующего набора в JPA.
5. Заключение
В этой статье мы показали, что собственные запросы являются общим местом для получения этого ClassCastException . Мы также рассмотрели возможность выполнения проверки типа самостоятельно, а также ее решения путем сопоставления результатов запроса с транспортным объектом.
Как всегда, полный исходный код примеров доступен на GitHub .