Fixing the JPA error “java.lang.String cannot be cast to Ljava.lang.String;” – 修复JPA错误 “java.lang.String不能被投到Ljava.lang.String;&#8221

最后修改: 2018年 11月 20日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

Of course, we’d never suppose that we can cast a String to a String array in Java:

当然,我们永远不会认为我们可以在Java中把一个字符串投向一个字符串数组。

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

But, this turns out to be a common JPA error.

但是,这原来是一个常见的JPA错误。

In this quick tutorial, we’ll show how this comes up and how to solve it.

在这个快速教程中,我们将展示这种情况是如何出现的,以及如何解决它。

2. Common Error Case in JPA

2.JPA中的常见错误案例

In JPA it’s not uncommon to get this error when we work with native queries and we use the createNativeQuery method of the EntityManager.

在JPA中,当我们使用本地查询并使用EntityManagercreateNativeQuery方法时,经常会出现这样的错误

Its Javadoc actually warns us that this method will return a list of Object[], or just an Object if only one column is returned by the query.

Javadoc实际上警告我们,该方法将返回一个Object[]的列表,如果查询只返回一列,则只返回一个Object

Let’s see an example. First, let’s create a query executor that we want to reuse to execute all of our queries:

让我们看一个例子。首先,让我们创建一个查询执行器,我们要重复使用它来执行我们所有的查询。

public class QueryExecutor {
    public static List<String[]> executeNativeQueryNoCastCheck(String statement, EntityManager em) {
        Query query = em.createNativeQuery(statement);
        return query.getResultList();
    }
}

As seen above, we’re using the createNativeQuery() method and we always expect a result set that contains a String array.

如上所述,我们正在使用createNativeQuery()方法,我们总是期望一个包含String数组的结果集。

After that, let’s create a simple entity to use in our examples:

之后,让我们创建一个简单的实体,在我们的例子中使用。

@Entity
public class Message {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String text;

    // getters and setters

}

And finally, let’s create a test class that inserts a Message before running the tests:

最后,让我们创建一个测试类,在运行测试前插入一个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();
    }
}

Now, we can use our QueryExecutor to execute a query that retrieves the text field of our entity:

现在,我们可以使用我们的QueryExecutor来执行一个查询,检索我们实体的text字段。

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

As we can see, because there is only one column in the query, JPA will actually return a list of strings, not a list of string arrays. We get a ClassCastException because the query returns a single column and we were expecting an array.

我们可以看到,由于查询中只有一列,JPA实际上将返回一个字符串列表,而不是一个字符串数组列表。我们得到了一个ClassCastException,因为查询返回的是一个单列,而我们期待的是一个数组。

3. Manual Casting Fix

3.手动浇注固定

The simplest way to fix this error is to check the type of the result set objects in order to avoid the ClassCastException. Let’s implement a method to do so in our QueryExecutor:

解决这个错误的最简单方法是检查结果集对象的类型,以避免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;
    }
}

Then, we can use this method to execute our query without getting an exception:

然后,我们可以使用这个方法来执行我们的查询,而不会得到一个异常。

@Test
public void givenExecutorWithCastCheck_whenQueryReturnsOneColumn_thenNoClassCastThrown() {
    List<String[]> results = QueryExecutor.executeNativeQueryWithCastCheck("select text from message", em);
    assertEquals("text", results.get(0)[0]);
}

This is not an ideal solution since we have to convert the result to an array in case the query returns only one column.

这不是一个理想的解决方案,因为我们必须将结果转换为数组,以防查询只返回一列。

4. JPA Entity Mapping Fix

4.JPA实体映射修复

Another way to fix this error is by mapping the result set to an entity. This way, we can decide how to map the results of our queries in advance and avoid unnecessary castings.

解决这个错误的另一个方法是将结果集映射到一个实体。这样,我们可以提前决定如何映射我们的查询结果,避免不必要的投递。

Let’s add another method to our executor to support the usage of custom entity mappings:

让我们为我们的执行器添加另一个方法,以支持自定义实体映射的使用。

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

After that, let’s create a custom SqlResultSetMapping to map the result set of our previous query to a Message entity:

之后,让我们创建一个自定义的SqlResultSetMapping,将我们之前查询的结果集映射到一个Message实体。

@SqlResultSetMapping(
  name="textQueryMapping",
  classes={
    @ConstructorResult(
      targetClass=Message.class,
      columns={
        @ColumnResult(name="text")
      }
    )
  }
)
@Entity
public class Message {
    // ...
}

In this case, we also have to add a constructor that matches our newly created SqlResultSetMapping:

在这种情况下,我们还必须添加一个构造函数来匹配我们新创建的SqlResultSetMapping

public class Message {

    // ... fields and default constructor

    public Message(String text) {
        this.text = text;
    }

    // ... getters and setters

}

Finally, we can use our new executor method to run our test query and get a list of Message:

最后,我们可以使用我们新的执行器方法来运行我们的测试查询,并获得一个Message的列表。

@Test
public void givenExecutorGeneric_whenQueryReturnsOneColumn_thenNoClassCastThrown() {
    List<Message> results = QueryExecutor.executeNativeQueryGeneric(
      "select text from message", "textQueryMapping", em);
    assertEquals("text", results.get(0).getText());
}

This solution is much cleaner since we delegate the result set mapping to JPA.

由于我们将结果集映射委托给JPA,这个解决方案要干净得多。

5. Conclusion

5.结论

In this article, we’ve shown that native queries are a common place to get this ClassCastException. We also looked at doing the type check ourselves as well as solving it by mapping the query results to a transport object.

在这篇文章中,我们展示了本地查询是获得这种ClassCastException的常见场所。我们还研究了自己做类型检查,以及通过将查询结果映射到传输对象来解决这个问题。

As always, the full source code of the examples is available over on GitHub.

一如既往,这些示例的完整源代码可在GitHub上获得