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

时间:2017-06-05 23:16:08

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

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

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

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

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

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

    我的研究:

1 个答案:

答案 0 :(得分:2)

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

<强> @EntityGraph

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

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

此方法适用于存储库的所有“读取”方法。

<强>缓存

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

假设我们有预订实体来存储图书数据,阅读实体可以存储有关特定图书的阅读次数及其阅读器评级的信息。要获得此数据,我们可以进行以下投影:

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

在更新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