Spring Data JPA接口投影嵌套关联:如何避免全实体查询?
问题背景
假设存在两个实体:
Author 实体
@Entity @Data @AllArgsConstructor @NoArgsConstructor public class Author { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String firstName; private String lastName; @ManyToMany(mappedBy = "authors") private Set<Book> books; }
Book 实体
@Entity @Data @AllArgsConstructor @NoArgsConstructor public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String fileName; private String fileType; @Lob private byte[] data; @ManyToMany @JoinTable( name = "book_authors", joinColumns = @JoinColumn(name = "book_id"), inverseJoinColumns = @JoinColumn(name = "author_id")) private Set<Author> authors; }
我们使用以下DTO接口投影来仅查询所需列:
public interface AuthorView { String getFirstName(); String getLastName(); Set<BookView> getBooks(); interface BookView { String getTitle(); } }
在仓库中声明了一个简单的findAllBy查询方法:
public interface AuthorRepository extends JpaRepository<Author, Long> { @EntityGraph(attributePaths = "books") List<AuthorView> findAllBy(); }
该方法执行以下SQL查询:
select author0_.id as id1_0_0_, book2_.id as id1_1_1_, author0_.first_name as first_na2_0_0_, author0_.last_name as last_nam3_0_0_, book2_.data as data2_1_1_, book2_.file_name as file_nam3_1_1_, book2_.file_type as file_typ4_1_1_, book2_.title as title4_1_1_, books1_.author_id as author_i2_2_0__, books1_.book_id as book_id1_2_0__ from author author0_ left outer join book_authors books1_ on author0_.id=books1_.author_id left outer join book book2_ on books1_.book_id=book2_.id
尽管投影中并未包含data、fileName和fileType属性,但这些字段仍会从数据库中获取,这会引发性能问题,尤其是当文件较大时。
根据Thorben Janssen的分析,问题根源在于Spring Data JPA会获取整个实体并进行编程式映射。
请问除了编写大量自定义查询外,是否有其他解决方案可以在使用基于接口的DTO投影时避免获取整个实体?
解决方案
1. 优化@EntityGraph的关联加载策略
在Author实体的books关联上添加@Fetch(FetchMode.SELECT),让Spring Data JPA拆分查询:先获取Author的所需字段,再单独查询关联Book的投影字段,而非一次性拉取所有Book字段。
修改Author实体的books字段:
@ManyToMany(mappedBy = "authors") @Fetch(FetchMode.SELECT) private Set<Book> books;
仓库方法保持不变,此时生成的SQL会拆分为两个查询:
- 第一个查询获取Author的id、firstName、lastName
- 第二个查询根据Author的id关联查询Book的id和title,不会拉取data、fileName、fileType
2. 类投影配合JPQL构造器查询
改用类投影,通过自定义JPQL精准指定要查询的字段,避免冗余数据:
首先定义DTO类:
@Data public class AuthorDto { private String firstName; private String lastName; private Set<BookDto> books; public AuthorDto(String firstName, String lastName, Set<BookDto> books) { this.firstName = firstName; this.lastName = lastName; this.books = books; } @Data public static class BookDto { private String title; public BookDto(String title) { this.title = title; } } }
然后在仓库中编写JPQL查询:
public interface AuthorRepository extends JpaRepository<Author, Long> { @Query("SELECT new com.yourpackage.AuthorDto(a.firstName, a.lastName, " + "CAST(COLLECT(new com.yourpackage.AuthorDto.BookDto(b.title)) AS java.util.Set)) " + "FROM Author a LEFT JOIN a.books b GROUP BY a.id, a.firstName, a.lastName") List<AuthorDto> findAllAuthorsWithBookTitles(); }
3. 使用Blaze-Persistence Entity Views扩展
Blaze-Persistence是JPA的扩展库,专门优化投影和DTO映射,自动生成仅包含所需字段的SQL:
首先引入Maven依赖:
<dependency> <groupId>com.blazebit</groupId> <artifactId>blaze-persistence-integration-spring-data-jpa</artifactId> <version>1.6.9</version> </dependency>
定义Entity View:
@EntityView(Author.class) public interface AuthorView { @IdMapping Long getId(); String getFirstName(); String getLastName(); @Mapping("books") Set<BookView> getBooks(); @EntityView(Book.class) interface BookView { String getTitle(); } }
仓库中使用:
public interface AuthorRepository extends JpaRepository<Author, Long>, EntityViewRepository<Author, Long> { List<AuthorView> findAll(); }
4. 大字段延迟加载
将Book实体中的大字段设置为延迟加载,这样Spring Data JPA映射投影时不会触发这些字段的加载:
@Lob @Basic(fetch = FetchType.LAZY) private byte[] data;
保持原有的接口投影和@EntityGraph配置即可,注意需确保在事务上下文外访问投影时不会触发懒加载异常。
内容的提问来源于stack exchange,提问作者Toni
相关产品推荐
相关产品推荐

