Persistence top

I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE COURSE

1. Introduction

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

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

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

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

2. Common Error Case in 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.

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.

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.

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:

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:

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

3. Manual Casting Fix

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:

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

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(
  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:

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:

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

5. Conclusion

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.

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

Persistence bottom

I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE LESSONS

Leave a Reply

avatar
  Subscribe  
Notify of