【问题标题】:How to avoid N+1 queries with Spring Data REST Projections?如何避免使用 Spring Data REST Projections 进行 N+1 查询?
【发布时间】:2017-11-06 19:47:40
【问题描述】:

我一直在 Spring Data JPA 和 Hibernate 支持的 Spring Data REST 中对我的新应用程序进行原型设计,这对我的团队来说是一个极好的生产力助推器,但随着数据模型变得越来越复杂,性能正在下降。查看执行的 SQL,我看到两个独立但相关的问题:

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

  2. 似乎没有办法使用 JPA 指定预先加载,因为 SDR 会自动生成存储库方法,因此我无法将 @EntityGraph 添加到它们。 (并且根据下面的 DATAREST-905,即使这样也行不通)编辑:在 Cepr0 下面的回答中解决,尽管这只能应用于每个 finder 方法一次。见DATAJPA-749

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

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

我的研究:

【问题讨论】:

标签: spring-data-jpa spring-data-rest


【解决方案1】:

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

@EntityGraph

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

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

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

缓存

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

假设我们有 Book 实体来存储图书数据和 Reading 实体来存储有关特定图书的阅读次数及其读者评分的信息。为了得到这些数据,我们可以像这样进行投影:

@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 的所有后续请求都将从我们的缓存中获取评分数据,不会导致对数据库的冗余请求。

更多信息和工作示例是here

【讨论】:

    猜你喜欢
    • 2013-03-30
    • 1970-01-01
    • 1970-01-01
    • 2020-09-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-26
    • 1970-01-01
    相关资源
    最近更新 更多