如何使用Spring Data REST投影避免N+1查询?

9

我一直在使用Spring Data JPA&Hibernate支持的Spring Data REST原型化我的新应用程序,这对我的团队是一个极好的生产力提升器,但是随着数据模型变得更加复杂,性能正在下降。查看执行的SQL时,我发现有两个独立但相关的问题:

  1. 当仅使用少量属性的Projection来减小有效载荷大小时,SDR仍会加载整个实体图,带来了所有相应的开销。 编辑:已提交DATAREST-1089

  2. 似乎没有办法使用JPA指定急切加载,因为SDR自动生成存储库方法,所以我无法将@EntityGraph添加到它们中(根据下面的DATAREST-905,即使那也不起作用)。 编辑:Cepr0的答案中解决了这个问题,尽管每个finder方法只能应用一次。请参见DATAJPA-749

我有一个关键模型,根据不同的上下文(列表页、查看页、自动完成、相关项目页等)使用几种不同的投影,因此实现一个自定义的ResourceProcessor似乎不是一个解决方案。
有人找到了解决这些问题的方法吗?否则,任何具有非平凡对象图的人都将看到性能急剧下降,因为他们的模型会增长。
我的研究:

https://jira.spring.io/browse/DATAREST-1089 - 这就是我们一直在等待的,对吧? - Cipous
1个回答

4
为了解决1+N问题,我使用以下两种方法:
@EntityGraph
在Repository的findAll方法中,我使用@EntityGraph注释。只需要覆盖它:
@Override
@EntityGraph(attributePaths = {"author", "publisher"})
Page<Book> findAll(Pageable pageable);

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

缓存

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

假设我们有一个Book实体来存储图书数据,以及一个Reading实体来存储特定 Book 的阅读量和读者评级的信息。要获取这些数据,我们可以创建如下的投影:

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

    String getTitle();
    String getIsbn();

    @Value("#{@readingRepo.getBookRatings(target)}")
    Ratings getRatings();
}

readingRepo.getBookRatings是ReadingRepository的方法:

@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);

它还会返回一个存储“评分”信息的投影:
@JsonSerialize(as = Ratings.class)
public interface Ratings {

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

    @JsonProperty("readings")
    Integer getReadings();
}

请求/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;
    }
}

然后在 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);

当书籍数据更新时,实现缓存驱逐:

@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());
    }
}

现在,所有后续对/books?projection=bookRating的请求都将从我们的缓存中获取评分数据,并且不会导致冗余的数据库请求。
更多信息和工作示例请参见此处

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接