Reputation: 8858
Movie
model :
@Entity
public class Movie {
private Long id;
private String name;
private Date releaseDate;
private List<MovieCelebrity> movieCelebrities = new ArrayList<>();
// getters & setters
}
MovieCelebrity
model :
@Entity
public class MovieCelebrity extends DateAudit {
private Long id;
private String characterName;
private Movie movie;
// getters & setters
}
I want to return the id, name, releaseDate and characterName along in the response, something like this:
{
"id": 1,
"name": Scarface,
"releaseDate": 1983,
"characterName": "Tony Montana"
}
So i made the following query:
@Query("SELECT m.id, m.name, m.releaseDate, mc.characterName FROM Movie m JOIN m.movieCelebrities mc " +
"WHERE mc.celebrity.id = :id AND mc.role = :role")
Page<Movie> findMoviesByCelebrity(@Param("id") Long id, @Param("role") CelebrityRole role, Pageable pageable);
But I'm getting the following error in the response:
"java.base/[Ljava.lang.Object; cannot be cast to com.movies.mmdb.model.Movie"
There's a solution to create a constructor in movie with the parameter i need to return and make a query like:
@Query("SELECT new Movie(m.id, m.name, m.releaseDate, mc.characterName) FROM...)
But since characterName is in different model i can't make such a constructor.
Upvotes: 3
Views: 6470
Reputation: 10931
Essentially the question is about how to do a Projection from a JPA query to a return type with nested values. This is something that is not really there with JPA queries at the moment.
Besides DTOs, there are projection interfaces in Spring JPA that can actually handle a bit of nesting (see the Spring Docs). These would be a reasonably simple option, but you still can't easily coerce one into a Movie
.
The main other option at the moment is a ResultTransformer
back in Hibernate. For example this could be accessed by using a named JPA query and then dropping back to the Hibernate query API before running it.
This is the declared named query (slightly simplified against the available classes in the samples in the question):
@Entity
@NamedQuery(name = "Movie.byCelebrity",
query = "SELECT m.id, m.name, m.releaseDate, mc.characterName FROM Movie m JOIN m.movieCelebrities mc " +
"WHERE mc.role = :role")
public class Movie {
This can then be called with a result transformer like this:
List<Movie> movies = entityManager
.createNamedQuery("Movie.byCelebrity")
.setParameter("role", role)
.unwrap(org.hibernate.query.Query.class)
.setResultTransformer(new MovieResultTransformer())
.getResultList();
Normally, for a single level query, you can use the AliasToBeanResultTransformer
that comes with Hibernate, but this won't merge or group the results by Movie
.
Here's an example result transformer to first map the return columns (in the 'tuple' which is a list of result fields), and then merge those by Movie
:
public class MovieResultTransformer
implements ResultTransformer {
@Override
public Object transformTuple(Object[] tuple,
String[] aliases) {
Movie movie = new Movie();
movie.setId((Long) tuple[0]);
movie.setName((String) tuple[1]);
movie.setReleaseDate((Date) tuple[2]);
MovieCelebrity movieCelebrity = new MovieCelebrity();
movieCelebrity.setCharacterName((String) tuple[3]);
movie.getMovieCelebrities().add(movieCelebrity);
return movie;
}
@Override
public List transformList(List collection) {
Map<Long, Movie> movies = new LinkedHashMap<>();
for (Object item : collection) {
Movie movie = (Movie) item;
Long id = movie.getId();
Movie existingMovie = movies.get(id);
if (existingMovie == null)
movies.put(id, movie);
else
existingMovie.getMovieCelebrities()
.addAll(movie.getMovieCelebrities());
}
return new ArrayList<>(movies.values());
}
}
It is worth noting that ResultTransformer
was deprecated with Hibernate 5.2, with the wonderful comment in the source: @todo develop a new approach to result transformers
.
Clearly the area of Projections in JPA is still a bit incomplete. The suggestion for Hibernate 6 is that they'll switch to a functional interface and lambda style API which would be a great improvement - it would be good to see something similar ripple into JPA.
Upvotes: 2
Reputation: 36103
The constructor expression with NEW can only be used with DTOs (Data Transfer Objects).
But the solution is much simpler. Simply return the movie like:
@Query("SELECT m FROM Movie m JOIN m.movieCelebrities mc " +
"WHERE mc.celebrity.id = :id AND mc.role = :role")
Page<Movie> findMoviesByCelebrity(@Param("id") Long id, @Param("role") CelebrityRole role, Pageable pageable);
Or if you want to use a DTO:
@Query("SELECT NEW your.package.MovieDTO(m.id, m.name, m.releaseDate, mc.characterName) FROM Movie m JOIN m.movieCelebrities mc " +
"WHERE mc.celebrity.id = :id AND mc.role = :role")
Page<MovieDTO> findMoviesByCelebrity(@Param("id") Long id, @Param("role") CelebrityRole role, Pageable pageable);
The MovieDTO must have a constructor that takes all the arguments from the query with the matching types.
Upvotes: 2
Reputation: 211
create a transient getter method for characterName as follow :
public class Movie {
private String name;
@Transient
public String getCharacterName(){
return getMovieCelebrities().iterator().next().getCharacterName();
}
}
then use your constructor solution.
Upvotes: 0