1. 引言
在 Java 中,我们当然知道不能将一个 String
类型强转为 String[]
数组:
java.lang.String cannot be cast to [Ljava.lang.String;
✅ 但这个错误在 JPA 中却很常见。
本篇教程会带你了解这个错误是怎么发生的,以及如何解决它。
2. JPA 中的常见错误场景
在使用 JPA 原生查询时,这个错误非常容易出现,特别是当我们使用 EntityManager
的 createNativeQuery
方法时。
⚠️ 官方 Javadoc 明确指出:
该方法返回的是 Object[]
的列表,或者如果查询只返回一列,那结果就是 Object
本身。
来看个例子。
2.1 查询执行器
我们先创建一个通用的查询执行器,用于复用:
public class QueryExecutor {
public static List<String[]> executeNativeQueryNoCastCheck(String statement, EntityManager em) {
Query query = em.createNativeQuery(statement);
return query.getResultList();
}
}
如上所示,我们使用了 createNativeQuery()
,并默认返回的是 String[]
列表。
2.2 实体类
接着创建一个简单的实体类:
@Entity
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String text;
// getters and setters
}
2.3 初始化测试数据
public class SpringCastUnitTest {
private static EntityManager em;
private static EntityManagerFactory emFactory;
@BeforeClass
public static void setup() {
emFactory = Persistence.createEntityManagerFactory("jpa-h2");
em = emFactory.createEntityManager();
// 插入测试数据
Message message = new Message();
message.setText("text");
EntityTransaction tr = em.getTransaction();
tr.begin();
em.persist(message);
tr.commit();
}
}
2.4 执行查询并触发异常
@Test(expected = ClassCastException.class)
public void givenExecutorNoCastCheck_whenQueryReturnsOneColumn_thenClassCastThrown() {
List<String[]> results = QueryExecutor.executeNativeQueryNoCastCheck("select text from message", em);
// 报错:java.lang.String cannot be cast to [Ljava.lang.String;
for (String[] row : results) {
// do nothing
}
}
❌ **因为查询只返回一列,JPA 实际返回的是 List<String>
,而不是 List<String[]>
**,所以强转失败。
3. 手动类型检查修复
✅ 最简单的修复方式是:在处理结果前先判断类型。
我们在 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 实体映射修复
✅ 更推荐的方式是:将查询结果映射到实体类中。
我们可以定义一个 SqlResultSetMapping
,将结果映射到 Message
实体。
4.1 添加映射配置
@SqlResultSetMapping(
name="textQueryMapping",
classes={
@ConstructorResult(
targetClass=Message.class,
columns={
@ColumnResult(name="text")
}
)
}
)
@Entity
public class Message {
// ...
}
4.2 添加构造函数
public class Message {
// ... fields and default constructor
public Message(String text) {
this.text = text;
}
// ... getters and setters
}
4.3 使用映射查询
public static <T> List<T> executeNativeQueryGeneric(String statement, String mapping, EntityManager em) {
Query query = em.createNativeQuery(statement, mapping);
return query.getResultList();
}
4.4 测试映射结果
@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. 总结
在这篇文章中,我们展示了在使用 JPA 原生查询时常见的 ClassCastException
问题,并提供了两种解决方案:
- ✅ 手动类型判断 + 转换
- ✅ 使用
@SqlResultSetMapping
映射到实体类
虽然第一种方法简单粗暴,但第二种更符合 JPA 的设计思想,也更推荐使用。
📚 完整代码示例可在 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/persistence-modules/java-jpa