Jpa储存库查询-java.lang.Object;无法转换为模型

时间:2018-09-29 13:14:18

标签: java spring-boot spring-data-jpa spring-rest

Movie 模型:

@Entity
public class Movie {

    private Long id;
    private String name;
    private Date releaseDate;
    private List<MovieCelebrity> movieCelebrities = new ArrayList<>();

    // getters & setters
}

MovieCelebrity 模型:

@Entity
public class MovieCelebrity extends DateAudit {

    private Long id;
    private String characterName;
    private Movie movie;

    // getters & setters
}

我想在响应中返回 id 名称 releaseDate characterName ,例如这个:

{
    "id": 1,
    "name": Scarface,
    "releaseDate": 1983,
    "characterName": "Tony Montana"
}

所以我进行了以下查询:

@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);

但是响应中出现以下错误:

  

“ java.base / [Ljava.lang.Object;无法转换为com.movi​​es.mmdb.model.Movie”

有一种解决方案,可以在电影中创建一个我需要返回的参数的构造函数,并进行如下查询:

@Query("SELECT new Movie(m.id, m.name, m.releaseDate, mc.characterName) FROM...)

但是由于 characterName 处于不同的模型中,所以我无法创建这样的构造函数。

3 个答案:

答案 0 :(得分:2)

带有NEW的构造函数表达式只能与DTO(数据传输对象)一起使用。

但是解决方案要简单得多。只需像这样返回电影即可:

@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);

或者如果您想使用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);

MovieDTO必须具有一个构造函数,该构造函数可以从查询中获取具有匹配类型的所有参数。

答案 1 :(得分:2)

本质上,问题是关于如何将JPA查询中的Projection转换为具有嵌套值的返回类型。目前,JPA查询还没有真正做到这一点。

除了DTO,Spring JPA中还有一些投影接口,它们实际上可以处理一些嵌套(请参见Spring Docs)。这些可能是一个相当简单的选项,但您仍然不能轻易将其强制为Movie

当前的另一个主要选择是ResultTransformer回到Hibernate。例如,可以通过使用命名的JPA查询并在运行它之前放回Hibernate查询API来访问它。

这是声明的命名查询(相对于问题示例中的可用类略有简化):

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

然后可以使用如下结果转换器调用它:

    List<Movie> movies = entityManager
            .createNamedQuery("Movie.byCelebrity")
            .setParameter("role", role)
            .unwrap(org.hibernate.query.Query.class)
            .setResultTransformer(new MovieResultTransformer())
            .getResultList();

通常,对于单级查询,您可以使用Hibernate随附的AliasToBeanResultTransformer,但这不会合并或按Movie分组结果。

这里是一个示例结果转换器,它首先映射返回列(在“ tuple”中,这是结果字段的列表),然后通过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());
    }
}

值得注意的是,ResultTransformer在Hibernate 5.2中是deprecated,在源代码@todo develop a new approach to result transformers中有很多精彩的注释。

显然,JPA中的Projections区域仍然有些不完整。对于Hibernate 6的建议是,他们将切换到功能接口和lambda样式的API,这将是一个很大的改进-希望看到类似的涟漪作用到JPA中。

答案 2 :(得分:0)

为characterName创建一个瞬态getter方法,如下所示:

public class Movie {
private String name;
@Transient
public String getCharacterName(){
return getMovieCelebrities().iterator().next().getCharacterName();

}
}

然后使用您的构造函数解决方案。