将使用ManyToMany关系的SQL查询转换为JPQL查询

时间:2018-09-20 02:56:45

标签: java spring hibernate spring-boot jpa

我正在尝试将以下SQL查询转换为JPQL查询:

SELECT *
FROM movie m INNER JOIN movie_genre mg ON m.id = mg.movie_id
WHERE mg.genre_id = (SELECT mg2.genre_id FROM movie_genre mg2 WHERE mg2.movie_id = ?1 AND mg2.movie_id <> mg.movie_id AND mg.genre_id = mg2.genre_id)
GROUP BY mg.movie_id ORDER BY count(*) DESC

问题是我没有模型类来表示movie_genre表,因为它是根据ManyToMany关系自动生成的表。

有什么办法可以将该查询转换为JPQL

此刻,我正在使用本机查询:

@Query(value = "SELECT * FROM movie m INNER JOIN movie_genre mg ON m.id = mg.movie_id " +
            "WHERE mg.genre_id = (SELECT mg2.genre_id FROM movie_genre mg2 WHERE mg2.movie_id = ?1 AND mg2.movie_id <> mg.movie_id AND mg.genre_id = mg2.genre_id) " +
            "GROUP BY mg.movie_id ORDER BY count(*) DESC", nativeQuery = true)
Page<Movie> findRelatedMoviesToAMovieById(@Param("id") int id, Pageable pageable);

编辑: 这是模型:
电影

@Entity
public class Movie extends DateAudit  {

    private Long id;
    private String name;
    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
     @JoinTable(name = "movie_genre",
        joinColumns = @JoinColumn(name = "movie_id"),
        inverseJoinColumns = @JoinColumn(name = "genre_id")
    )
    private List<Genre> genres = new ArrayList<>();
}

类型

@Entity
public class Genre {

    private Long id;
    private String name;
    @ManyToMany(mappedBy = "genres", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private Set<Movie> movies = new HashSet<>();
}

2 个答案:

答案 0 :(得分:1)

您不需要模型movie_genre即可编写JPQL语句。 Hibernate在执行JPQL语句时暗中了解它。

示例:

SELECT m from Movie m left join m.genres g where g.name = 'Action'

双向“ m.genres g”可用于所有双向JPQL语句,包括多对多的关联模型是隐式的而实际上不存在的。

示例代码:

@Entity
@Table(name = "MOVIE")
public class Movie {

@Id
@GeneratedValue
private Long id;

@Column
private String name;

@ManyToMany(cascade = {CascadeType.ALL})
@JoinTable(name = "movie_genre",
        joinColumns = @JoinColumn(name = "movie_id"),
        inverseJoinColumns = {@JoinColumn(name = "genre_id")}
)
private Set<Genre> genres = new HashSet<>();

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}


public Set<Genre> getGenres() {
    return genres;
}

public void setGenres(Set<Genre> genres) {
    this.genres = genres;
}
}

@Entity
@Table(name = "GENRE")
public class Genre {

@Id
@GeneratedValue
private Long id;

@Column
private String name;


@ManyToMany(mappedBy = "genres", cascade = {CascadeType.ALL})
private Set<Movie> movies = new HashSet<>();

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public Set<Movie> getMovies() {
    return movies;
}

public void setMovies(Set<Movie> movies) {
    this.movies = movies;
}
}

有效的JPQL示例

    @PersistenceContext
EntityManager entityManager;

@Test
@Transactional
public void test() {
    Set<Movie> actionMovies = new HashSet<>();
    Set<Movie> dramaMovies = new HashSet<>();
    Set<Genre> dramaGenres = new HashSet<>();
    Set<Genre> actionGenres = new HashSet<>();
    Set<Genre> generes = new HashSet<>();

    Movie actionMovie = new Movie();
    actionMovie.setName("Batman");
    actionMovies.add(actionMovie);

    Movie dramaMovie = new Movie();
    dramaMovie.setName("Forest Gump");
    dramaMovies.add(dramaMovie);

    Genre actionGenre = new Genre();
    actionGenre.setName("Action");
    generes.add(actionGenre);
    actionGenres.add(actionGenre);

    Genre dramaGenre = new Genre();
    dramaGenre.setName("Drama");
    generes.add(dramaGenre);
    dramaGenres.add(dramaGenre);

    //Bidirectional sets
    actionGenre.setMovies(actionMovies);
    dramaGenre.setMovies(dramaMovies);
    actionMovie.setGenres(actionGenres);
    dramaMovie.setGenres(dramaGenres);

    genreRepository.saveAll(generes);

    //Example JPQL join through not present association model.
    Query query = entityManager.createQuery("SELECT m from Movie m left join m.genres g where g.name = 'Action'");
    List<Movie> resultList = query.getResultList();
    assertEquals("Batman",resultList.get(0).getName());


}

答案 1 :(得分:1)

尽管您提供了SQL查询,但我还是重新考虑了您的要求,并将其转换为JPQL。到目前为止,据我了解,您正在查找具有相同类型的电影的相关电影。如果这是正确的,您可以通过

SELECT m FROM Movie m JOIN m.genres mg 
WHERE mg.id IN 
         (SELECT g.id FROM Genre g JOIN g.movies gm WHERE gm.id = :movieId) 
AND m.id <> :movieId 
GROUP BY m ORDER BY count(*) DESC

这将生成如下的SQL

 select distinct movie0_.id as id1_1_, movie0_.name as name2_1_ 
 from movie movie0_ inner join movie_genre genres1_ on movie0_.id=genres1_.movie_id inner join genre genre2_ on genres1_.genre_id=genre2_.id 
 where (genre2_.id in 
            (select genre3_.id from genre genre3_ inner join movie_genre movies4_ on genre3_.id=movies4_.genre_id inner join movie movie5_
             on  movies4_.movie_id=movie5_.id where movie5_.id=?)) 
 and movie0_.id <> ? 
 group by movie0_.id order by count(*) desc

在JPQL中查询集合关联

使用JPQL时,您位于“对象关系”世界中。因此,当您查询作为集合的关联时,可以通过该集合字段进行访问。并且,当它是ManyToMany关联时,因此您没有具有映射实体的联接表,则需要使用集合字段进行联接。而且JPA供应商会自动将其转换为与联接表联接。

就像您要查询某些类型的电影一样,

 SELECT m FROM Movie m JOIN m.genres mg WHERE mg.id = :genreId 

性能问题

正如您在生成的SQL中注意到的那样,有许多级别的联接可用于获取和过滤数据。这导致性能瓶颈。为了克服这个问题,您可以为movie_genre表提供实体。

这种情况在这里得到了公平的讨论

The best way to map a many-to-many association with extra columns when using JPA and Hibernate