You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 23:31:06