如何避免使用Spring Data REST Projections进行N + 1查询?

jos*_*hwa 5 spring-data-jpa spring-data-rest

我一直在以Spring Data JPA和Hibernate为后盾的Spring Data REST中对我的新应用程序进行原型设计,这对我的团队而言是极大的生产力提升,但是随着数据模型变得越来越复杂,性能将逐渐下降。查看执行的SQL,我看到两个单独但相关的问题:

  1. 当使用Projection仅具有几个属性的a来减小我的有效负载的大小时,SDR仍将加载整个实体图,并产生所有开销。编辑:提起DATAREST-1089

  2. 似乎没有办法使用JPA指定急切加载,因为SDR会自动生成存储库方法,因此我无法添加@EntityGraph它们。(并且按照下面的DATAREST-905,即使这样也不起作用)编辑:在Cepr0的下面的答案中解决了该问题,尽管这只能应用于每个finder方法。参见DATAJPA-749

我有一个关键模型,根据上下文(列表页面,视图页面,自动完成,相关项目页面等),我使用了几种不同的投影,因此实现一个自定义ResourceProcessor似乎不是一种解决方案。)

有没有人找到解决这些问题的方法?否则,拥有不平凡的对象图的任何人都会看到随着模型的增长性能急剧下降。

我的研究:

Cep*_*pr0 4

为了解决 1+N 问题,我使用以下两种方法:

@EntityGraph

我在方法存储库中使用“@EntityGraph”注释findAll。只需覆盖它:

@Override
@EntityGraph(attributePaths = {"author", "publisher"})
Page<Book> findAll(Pageable pageable);
Run Code Online (Sandbox Code Playgroud)

这种方式适用于Repository的所有“读”方法。

缓存

我使用缓存来减少 1+N 问题对复杂投影的影响。

假设我们有Book实体来存储书籍数据,而Reading实体则存储有关特定书籍的阅读次数及其读者评分的信息。为了获取这些数据,我们可以进行如下投影:

@Projection(name = "bookRating", types = Book.class)
public interface WithRatings {

    String getTitle();
    String getIsbn();

    @Value("#{@readingRepo.getBookRatings(target)}")
    Ratings getRatings();
}
Run Code Online (Sandbox Code Playgroud)

readingRepo.getBookRatingsReadingRepository的方法在哪里:

@RestResource(exported = false)
@Query("select avg(r.rating) as rating, count(r) as readings from Reading r where r.book = ?1")
Ratings getBookRatings(Book book);
Run Code Online (Sandbox Code Playgroud)

它还返回一个存储“评级”信息的投影:

@JsonSerialize(as = Ratings.class)
public interface Ratings {

    @JsonProperty("rating")
    Float getRating();

    @JsonProperty("readings")
    Integer getReadings();
}
Run Code Online (Sandbox Code Playgroud)

的请求/books?projection=bookRating将导致对每本书调用readingRepo.getBookRatings,这将导致冗余的N个查询。

为了减少这种影响,我们可以使用缓存

在SpringBootApplication类中准备缓存:

@SpringBootApplication
@EnableCaching
public class Application {

    //...

    @Bean
    public CacheManager cacheManager() {

        Cache bookRatings = new ConcurrentMapCache("bookRatings");

        SimpleCacheManager manager = new SimpleCacheManager();
        manager.setCaches(Collections.singletonList(bookRatings));

        return manager;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后在方法上添加相应的注解readingRepo.getBookRatings

@Cacheable(value = "bookRatings", key = "#a0.id")
@RestResource(exported = false)
@Query("select avg(r.rating) as rating, count(r) as readings from Reading r where r.book = ?1")
Ratings getBookRatings(Book book);
Run Code Online (Sandbox Code Playgroud)

并在 Book 数据更新时实现缓存驱逐:

@RepositoryEventHandler(Reading.class)
public class ReadingEventHandler {

    private final @NonNull CacheManager cacheManager;

    @HandleAfterCreate
    @HandleAfterSave
    @HandleAfterDelete
    public void evictCaches(Reading reading) {
        Book book = reading.getBook();
        cacheManager.getCache("bookRatings").evict(book.getId());
    }
}
Run Code Online (Sandbox Code Playgroud)

现在所有后续请求/books?projection=bookRating将从我们的缓存中获取评级数据,并且不会对数据库造成冗余请求。

更多信息和工作示例在这里