Corrigindo o erro JPA "java.lang.String não pode ser convertido para Ljava.lang.String;"
1. Introdução
Claro, nunca suporíamos que podemos lançar umString para umString array em Java:
java.lang.String cannot be cast to [Ljava.lang.String;
Porém, esse é um erro comum de JPA.
Neste tutorial rápido, mostraremos como isso surge e como resolvê-lo.
2. Caso de erro comum em JPA
Em JPA, não é incomum obter este errowhen we work with native queries and we use the createNativeQuery method of the EntityManager.
SeuJavadoc nos avisa quethis method will return a list of Object[], or just an Object if only one column is returned by the query.
Vamos ver um exemplo. Primeiro, vamos criar um executor de consulta que queremos reutilizar para executar todas as nossas consultas:
public class QueryExecutor {
public static List executeNativeQueryNoCastCheck(String statement, EntityManager em) {
Query query = em.createNativeQuery(statement);
return query.getResultList();
}
}
Como visto acima, estamos usando o métodocreateNativeQuery()e sempre esperamos um conjunto de resultados que contém uma matrizString.
Depois disso, vamos criar uma entidade simples para usar em nossos exemplos:
@Entity
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String text;
// getters and setters
}
E, finalmente, vamos criar uma classe de teste que insere umMessage antes de executar os testes:
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();
}
}
Agora, podemos usar nossoQueryExecutor para executar uma consulta que recupera o campotext de nossa entidade:
@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
}
}
Como podemos ver,because there is only one column in the query, JPA will actually return a list of strings, not a list of string arrays. Temos umClassCastException porque a consulta retorna uma única coluna e esperávamos um array.
3. Correção de fundição manual
The simplest way to fix this error is to check the type of the result set objects para evitar oClassCastException. Vamos implementar um método para fazer isso em nossoQueryExecutor:
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;
}
}
Em seguida, podemos usar esse método para executar nossa consulta sem obter uma exceção:
@Test
public void givenExecutorWithCastCheck_whenQueryReturnsOneColumn_thenNoClassCastThrown() {
List results = QueryExecutor.executeNativeQueryWithCastCheck("select text from message", em);
assertEquals("text", results.get(0)[0]);
}
Essa não é uma solução ideal, pois precisamos converter o resultado em uma matriz, caso a consulta retorne apenas uma coluna.
4. Correção de mapeamento de entidade JPA
Another way to fix this error is by mapping the result set to an entity. Dessa forma,we can decide how to map the results of our queries in advancee evite fundições desnecessárias.
Vamos adicionar outro método ao nosso executor para oferecer suporte ao uso de mapeamentos de entidades personalizadas:
public static List executeNativeQueryGeneric(String statement, String mapping, EntityManager em) {
Query query = em.createNativeQuery(statement, mapping);
return query.getResultList();
}
Depois disso, vamos criar um mapaSqlResultSetMapping para mapear o conjunto de resultados de nossa consulta anterior para uma entidadeMessage:
@SqlResultSetMapping(
name="textQueryMapping",
classes={
@ConstructorResult(
targetClass=Message.class,
columns={
@ColumnResult(name="text")
}
)
}
)
@Entity
public class Message {
// ...
}
Nesse caso, também temos que adicionar um construtor que corresponda ao nossoSqlResultSetMapping recém-criado:
public class Message {
// ... fields and default constructor
public Message(String text) {
this.text = text;
}
// ... getters and setters
}
Finalmente, podemos usar nosso novo método executor para executar nossa consulta de teste e obter uma lista deMessage:
@Test
public void givenExecutorGeneric_whenQueryReturnsOneColumn_thenNoClassCastThrown() {
List results = QueryExecutor.executeNativeQueryGeneric(
"select text from message", "textQueryMapping", em);
assertEquals("text", results.get(0).getText());
}
Essa solução é muito mais limpa, pois delegamos o mapeamento do conjunto de resultados à JPA.
5. Conclusão
Neste artigo, mostramos que as consultas nativas são um lugar comum para obter esseClassCastException. Também analisamos a verificação de tipo e resolvemos mapeando os resultados da consulta para um objeto de transporte.
Como sempre, o código-fonte completo dos exemplos está disponívelover on GitHub.