Загрузка JPA Entity Graph, на которую влияет предложение where - PullRequest
0 голосов
/ 17 июня 2020

У меня возникли проблемы с использованием функции JPA Entity Graph для загрузки данных ожидаемым образом, используя Spring Data JPA и Hibernate (с базой данных Postgres). Для иллюстрации я привел простой пример. У меня есть приложение Spring Boot, содержащее Статьи и Теги с отношением «многие ко многим» между ними:

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String title;

    @ManyToMany
    private Set<Tag> tags;
}
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Tag {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;
}

У меня также есть простой TagRepository, а также ArticleRepository для помощи при загрузке статей, используя аннотацию @EntityGraph, чтобы указать, какие свойства я хочу загружать, когда я использую каждый метод репозитория, в данном случае свойство tags:

@Repository
public interface ArticleRepository extends JpaRepository<Article, Integer> {
    @EntityGraph(attributePaths = {"tags"})
    Set<Article> findAllByTagsIn(Collection<Tag> tags);

    @Override
    @EntityGraph(attributePaths = {"tags"})
    List<Article> findAll();
}
@Repository
public interface TagRepository extends JpaRepository<Tag, Integer> {
}

Наконец, чтобы проиллюстрировать проблему, я выполняю код при запуске приложения, который:

  • Создает 3 тегов , Tag A, Tag B и Tag C
  • Создает 3 статей :
    • Article A, которым присваиваются теги A и B
    • Article B, которому присвоены теги A , B и C
    • Article C, которому назначен только тег C
  • Выполняет от ArticleRepository.findAllByTagsIn до получает все статьи, которые назначены Tag A, нетерпеливо загрузка тегов, а затем печать каждого название статьи и имена присвоенных ей тегов.
  • Повторяет предыдущий шаг, используя вместо него ArticleRepository.findAll.
@Component
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {
    private final ArticleRepository articleRepository;
    private final TagRepository tagRepository;

    @Autowired
    public StartupListener(ArticleRepository articleRepository, TagRepository tagRepository) {
        this.articleRepository = articleRepository;
        this.tagRepository = tagRepository;
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        Tag tagA = tagRepository.save(new Tag(null, "Tag A"));
        Tag tagB = tagRepository.save(new Tag(null, "Tag B"));
        Tag tagC = tagRepository.save(new Tag(null, "Tag C"));

        articleRepository.save(new Article(null, "Article A", Set.of(tagA, tagB)));
        articleRepository.save(new Article(null, "Article B", Set.of(tagA, tagB, tagC)));
        articleRepository.save(new Article(null, "Article C", Set.of(tagC)));

        printArticles("--- Find all by tags in ---", articleRepository.findAllByTagsIn(Set.of(tagA)));
        printArticles("--- Find all ---", articleRepository.findAll());
    }

    private void printArticles(String title, Collection<Article> articles) {
        System.out.println(title);
        articles.forEach(article -> {
            String tags = article.getTags().stream().map(Tag::getName).collect(Collectors.joining(", "));
            System.out.println(String.format("%s: [%s]", article.getTitle(), tags));
        });
    }
}

Expectation

Я ожидал, что в При первом запросе будут загружены только статьи A и B, но будут отображаться все присвоенных им тегов. Во втором запросе я ожидал, что будут загружены все статей с всеми связанных с ними тегов. Т.е. я ожидал, что результат будет выглядеть так:

--- Find all by tags in ---
Article A: [Tag A, Tag B]
Article B: [Tag A, Tag B, Tag C]
--- Find all ---
Article A: [Tag A, Tag B]
Article B: [Tag A, Tag B, Tag C]
Article C: [Tag C]

Результат

К моему удивлению, с дополнительным предложением where, добавленным к первому запросу, только теги, которые были в set, предоставленный в качестве аргумента для метода репозитория, действительно был загружен. В этом случае Tag B и Tag C были полностью опущены, потому что набор, содержащий только Tag A, был передан методу репозитория и последующему запросу, который был выполнен. Второй запрос сработал, как я и ожидал, потому что в результирующий запрос не было добавлено where предложений, окружающих категории:

--- Find all by tags in ---
Article A: [Tag A]
Article B: [Tag A]
--- Find all ---
Article A: [Tag A, Tag B]
Article B: [Tag A, Tag B, Tag C]
Article C: [Tag C]

В конечном итоге выполняются следующие запросы:

select
    article0_.id as id1_0_0_,
    tag2_.id as id1_2_1_,
    article0_.title as title2_0_0_,
    tag2_.name as name2_2_1_,
    tags1_.article_id as article_1_1_0__,
    tags1_.tags_id as tags_id2_1_0__
from
    article article0_
left outer join
    article_tags tags1_
    on article0_.id=tags1_.article_id
left outer join
    tag tag2_
    on tags1_.tags_id=tag2_.id
where
    tag2_.id in (?)
select
    article0_.id as id1_0_0_,
    tag2_.id as id1_2_1_,
    article0_.title as title2_0_0_,
    tag2_.name as name2_2_1_,
    tags1_.article_id as article_1_1_0__,
    tags1_.tags_id as tags_id2_1_0__
from
    article article0_
left outer join
    article_tags tags1_
    on article0_.id=tags1_.article_id
left outer join
    tag tag2_
    on tags1_.tags_id=tag2_.id

Я немного удивлен, что Hibernate пытается сделать все это с помощью одного запроса. Я ожидал, что он сначала выполнит начальный запрос, чтобы получить все совпадающие статьи, присоединиться к тегам и применить условие where для фильтрации тех, которые не соответствуют критериям; затем выполнить другой запрос, используя идентификаторы загруженных статей для загрузки всех связанных тегов.

Вопросы

  • Это Ожидаемое поведение JPA по умолчанию с Entity Graphs? Чтобы учесть любые предложения where при активной загрузке сущностей?
  • Как я могу достичь ожидаемого результата, когда отображаются только статьи, которым присвоено Tag A (статьи A и B), но все связанных с ними тегов быстро загружаются?

1 Ответ

1 голос
/ 19 июня 2020

Похоже, это проблема Spring-Data. Фильтр тегов не должен использовать тот же псевдоним соединения, который используется для выборки графа сущностей. Вы можете обойти это, используя Spring-Data Specification , который выполняет подзапрос EXISTS для реализации фильтра. Примерно так

@Repository
public interface ArticleRepository extends JpaRepository<Article, Integer> {
    @EntityGraph(attributePaths = {"tags"})
    Set<Article> findAll(Specification<Article> s);
    default Set<Article> findAllByTagsIn(Collection<Tag> tags) {
      return findAll((root, cq, cb) -> {
        Subquery<Integer> subquery = cq.subquery(Integer.class);
        subquery.select(cb.literal(1));
        subquery.where(subquery.correlate(root).<Tag>join("tags").in(tags));
        return cb.exists(subquery);
      });
    }
}
...