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<String[]> executeNativeQueryNoCastCheck(String statement, EntityManager em) {
Query query = em.createNativeQuery(statement);
return query.getResultList();
}
}
Как видно выше, мы используем метод createNativeQuery()
и всегда ожидаем набор результатов, содержащий массив строк .
После этого давайте создадим простую сущность для использования в наших примерах:
@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();
}
}
Теперь мы можем использовать наш QueryExecutor
для выполнения запроса, который извлекает текстовое
поле нашей сущности:
@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
}
}
Как мы видим, поскольку в запросе только один столбец, 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]);
}
Это не идеальное решение, так как нам нужно преобразовать результат в массив, если запрос возвращает только один столбец.
4. Исправление сопоставления объектов JPA
Другой способ исправить эту ошибку — сопоставить результирующий набор с сущностью. Таким образом, мы можем заранее решить, как отображать результаты наших запросов, и избежать ненужных преобразований.
Давайте добавим в наш исполнитель еще один метод для поддержки использования пользовательских сопоставлений сущностей:
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
}
Наконец, мы можем использовать наш новый метод-исполнитель для запуска нашего тестового запроса и получения списка сообщений
:
@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
. Мы также рассмотрели возможность самостоятельной проверки типов и решения этой проблемы путем сопоставления результатов запроса с транспортным объектом.
Как всегда, полный исходный код примеров доступен на GitHub .