首页 > 解决方案 > 多个@OneToMany实体和内部实体本地化的Spring数据JPA派生查询

问题描述

我正在尝试使用 Spring Data JPA 派生查询执行一项简单的任务,但无法从查询中获得所需的结果。基本上我有一本书,它可以有一个或多个章节,并支持该书和章节的本地化。我想创建一个查询,该查询将基于语言环境获取特定于语言的书籍(带有章节)。这是我的四个实体。

@Entity
@Getter
@Setter
public class Book {

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private int noOfPages;

    /**
     * Both mappings below are unidirectional @OneToMany
     */
    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "BOOK_ID", referencedColumnName = "ID")
    private List<BookTranslation> bookTranslations;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "BOOK_ID", referencedColumnName = "ID")
    private List<Chapter> chapters;

    /**
     * Constructor for JPA
     */
    protected Book() {
    }

    public Book(int noOfPages, List<BookTranslation> bookTranslations, List<Chapter> chapters) {
        this.noOfPages = noOfPages;
        this.bookTranslations = bookTranslations;
        this.chapters = chapters;
    }
}

@Entity
@Getter
@Setter
public class BookTranslation {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(EnumType.STRING)
    private Language language;

    private String name;

    /**
     * Constructor for JPA
     */
    protected BookTranslation() {
    }

    public BookTranslation(Language language, String name) {
        this.language = language;
        this.name = name;
    }
}


@Entity
@Getter
@Setter
public class Chapter {

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private int chapterNumber;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "CHAPTER_ID", referencedColumnName = "ID")
    private List<ChapterTranslation> chapterTranslations;

    /**
     * Constructor for JPA
     */
    protected Chapter() {
    }

    public Chapter(int chapterNumber, List<ChapterTranslation> chapterTranslations) {
        this.chapterNumber = chapterNumber;
        this.chapterTranslations = chapterTranslations;
    }
}

@Entity
@Getter
@Setter
public class ChapterTranslation {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(EnumType.STRING)
    private Language language;

    private String title;

    /**
     * Constructor for JPA
     */
    protected ChapterTranslation() {
    }

    public ChapterTranslation(Language language, String title) {
        this.language = language;
        this.title = title;
    }
}

public enum Language {
    EN, FR
}



下面是示例代码,我用来持久化这些实体。请忽略@GetMapping,这只是一个示例。


    @GetMapping("/persist-book")
    public void persistBook() {
        ChapterTranslation enChapter = new ChapterTranslation(Language.EN, "What is Java persistence?");
        ChapterTranslation frChapter = new ChapterTranslation(Language.FR, "Qu'est-ce que la persistance Java?");
        List<ChapterTranslation> chapterOneTranslation = new ArrayList<>();
        chapterOneTranslation.add(enChapter);
        chapterOneTranslation.add(frChapter);
        Chapter chapterOne = new Chapter(1, chapterOneTranslation);
        List<Chapter> chapters = new ArrayList<>();
        chapters.add(chapterOne);

        BookTranslation enBook = new BookTranslation(Language.EN, "JPA WikiBook in English");
        BookTranslation frBook = new BookTranslation(Language.FR, "JPA WikiBook in French");
        List<BookTranslation> bookTranslations = new ArrayList<>();
        bookTranslations.add(enBook);
        bookTranslations.add(frBook);
        Book book = new Book(500, bookTranslations, chapters);
        bookRepository.save(book);
    }

我的 BookRepository 如下所示:

public interface BookRepository extends CrudRepository<Book, Long> {

    List<Book> findBooksByBookTranslations_LanguageAndChapters_ChapterTranslations_Language(Language lang1, Language lang2);
}

我用来检索结果的示例代码。

@GetMapping("/english-book")
    public List<Book> retrieveEnglishBook() {
        return bookRepository.findBooksByBookTranslations_LanguageAndChapters_ChapterTranslations_Language(
                Language.EN, Language.EN
        );
    }

我的预期输出如下图所示。

在此处输入图像描述

我从 Hibernate 日志中注意到的一件事是 Hibernate 总共进行了四个选择查询,而第一个查询输出正是我所需要的。但是,由于这是一个基于方法名称的查询,我认为我无法控制它。

编辑1:在尝试答案之前,我得到了所有返回所有语言环境的书籍,在将我的查询更改为接受的答案中给出的一个之后,我能够获得具有所选语言环境的书籍。

请注意:我还必须将所有集合从使用列表更改为集合,有关此内容的更多信息可以在接受的答案链接中阅读。

标签: spring-bootjpaspring-data-jpa

解决方案


您描述为所需结果的是单个数据库结果。我猜你的意思是你希望得到所有的书,但只有一种语言的翻译。

您没有描述您实际获得的内容,因此假设您正在获得包含所有可用翻译的书。

您想要的结果超出了派生查询的能力。派生查询的不同谓词都限制了在您的情况下要返回的根实体Book。他们仍然应该保留所有参考资料。

您可以使用这样的带注释查询来实现您的目标:

public interface BookRepository extends CrudRepository<Book, Long> {

    @Query("SELECT b FROM Book b 
        JOIN FETCH b.bookTranslations as bt
        JOIN FETCH b.chapter as c
        JOIN FETCH c.chapterTranslation as ct
        WHERE bt.language = :lang   
        AND ct.language = :lang")
    List<Book> findBooksByLanguage(Language lang);
}

另请参阅如何在 JPQL 查询中过滤子集合?

旁注:只有在结果方法名称与您无论如何命名该方法的名称非常相似时,才应使用查询派生。


推荐阅读