JPA/Hibernate n+1 problem, fetchmode join vs fetchtype eager

0

Cześć,

czytam sobie o problemie n+1 w Hibernate i po pierwsze nie jestem w stanie sprawić żeby u mnie w apce wystąpił ten problem. Przykładowo, stworzyłem sobie dwie klasy: User i Feature z relacją undirectional, czyli tylko w User entity mam:

@OneToMany
private List<Feature> features = new ArrayList<>();

Dodaję sobie jednego usera i kilka featerow przy odpalaniu apki w klasie Bootstrap. I mamy sobie klasę jakiśService gdzie w pętli wypisuje właściwości featerow (nie zwracajcie uwagi na sposób pobierania optionala) czyli:

List<Feature> features = getUserById(id).get().getFeatures();
for (Feature feature : features) {
  String description = feature.getDescription();
  System.out.println( description);
}

Dodałem w application.properties

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

I odpalam requesta żeby się ta metoda serwisowa uruchomiła to w logach mam:

Hibernate: 
    select
        user0_.id as id1_1_0_,
        user0_.first_name as first_na2_1_0_,
        user0_.last_name as last_nam3_1_0_ 
    from
        "user" user0_ 
    where
        user0_.id=?
Hibernate: 
    select
        features0_."user_id" as user_id1_2_0_,
        features0_.features_id as features2_2_0_,
        feature1_.id as id1_0_1_,
        feature1_.description as descript2_0_1_,
        feature1_.enabled as enabled3_0_1_,
        feature1_.feature_group as feature_4_0_1_,
        feature1_.name as name5_0_1_ 
    from
        "user_features" features0_ 
    inner join
        feature feature1_ 
            on features0_.features_id=feature1_.id 
    where
        features0_."user_id"=?

To gdzie występuje problem w postaci osobnego zapytania do każdego z dzieci?
Powinno być coś takiego (przykład z jakiejś strony):

SELECT
    pc.id AS id1_1_,
    pc.post_id AS post_id3_1_,
    pc.review AS review2_1_
FROM
    post_comment pc
 
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4

Druga sprawa to natrafiłem na rozwiązania typu:

  1. @BatchSize
  2. @Fetch(FetchMode.JOIN)

Pytanie co do drugiej opcji z fetchem. Z tego co zrozumiałem dodanie @Fetch(JOIN) powoduje, że kolekcja jest pobierana od razu (eager, nie lazy). Skoro przy lazy loadingu mamy problem n+1 i załatwiamy go zmieniając pośrednio na EAGER (przez fetch) to czemu jest on polecany do dużych kolekcji ze względu na performance jak paradoksalnie tworzy to jeszcze większy problem wydajnościowy?

1

na ile ja kibicuję problemowi, to problem N+1 nie "musi" zajść ale "może", jeśli silnikowi JPA nie uda się odgadnąć i zaoptymalizowac intencji klienta

0

Twój user jest jeden, więc problem zmienia się w 1 + n :) Żeby faktycznie przekonać się o tym problemie, pobierz listę userów wraz z ich relacjami. Mapowanie obiektowo-relacyjne jest pozornie wygodne, jednak stwarza problemy, które nie występują w przypadku zwykłego mapowania wyników SQL-a na obiekty. Beż ORM-a życie jest prostsze.

1

Nie ustawiłeś FetchMode -> domyślnie JOIN ( https://www.baeldung.com/hibernate-fetchmode). Warto doczytać jak działa FetchMode/FetchType.

Dodaj @Fetch(FetchMode.SELECT) i zobacz czy masz 1+N zapytań.

0
yarel napisał(a):

Nie ustawiłeś FetchMode -> domyślnie JOIN ( https://www.baeldung.com/hibernate-fetchmode). Warto doczytać jak działa FetchMode/FetchType.

Dodaj @Fetch(FetchMode.SELECT) i zobacz czy masz 1+N zapytań.

Przy @ Fetch(FetchMode.SELECT) jest dokładnie tak samo jak bez (to co podałem w pierwszym poście)

0
SkrzydlatyWąż napisał(a):

Twój user jest jeden, więc problem zmienia się w 1 + n :) Żeby faktycznie przekonać się o tym problemie, pobierz listę userów wraz z ich relacjami. Mapowanie obiektowo-relacyjne jest pozornie wygodne, jednak stwarza problemy, które nie występują w przypadku zwykłego mapowania wyników SQL-a na obiekty. Beż ORM-a życie jest prostsze.

Zmieniłem mapowanie. Teraz mam w klasie User:

@ManyToMany(mappedBy = "users")
private Set<Feature> features = new HashSet<>();

oraz w klasie Feature:

@ManyToMany
@JoinTable(name = "user_feature", joinColumns = @JoinColumn(name = "feature_id"),
        inverseJoinColumns = @JoinColumn(name = "user_id"))
@JsonIgnore
private Set<User> users = new HashSet<>();

Dodałem 5 userów do bazy i każdy user ma 5 featurów (takich samych).
Wysyłając GET request żeby pobrać listę użytkowników ze wszystkimi polami (list feature też) dostaję taki output z Hibernate:

Hibernate: 
    select
        user0_.id as id1_1_,
        user0_.first_name as first_na2_1_,
        user0_.last_name as last_nam3_1_ 
    from
        "user" user0_
Hibernate: 
    select
        features0_.user_id as user_id2_2_0_,
        features0_.feature_id as feature_1_2_0_,
        feature1_.id as id1_0_1_,
        feature1_.description as descript2_0_1_,
        feature1_.enabled as enabled3_0_1_,
        feature1_.feature_group as feature_4_0_1_,
        feature1_.name as name5_0_1_ 
    from
        user_feature features0_ 
    inner join
        feature feature1_ 
            on features0_.feature_id=feature1_.id 
    where
        features0_.user_id=?
Hibernate: 
    select
        features0_.user_id as user_id2_2_0_,
        features0_.feature_id as feature_1_2_0_,
        feature1_.id as id1_0_1_,
        feature1_.description as descript2_0_1_,
        feature1_.enabled as enabled3_0_1_,
        feature1_.feature_group as feature_4_0_1_,
        feature1_.name as name5_0_1_ 
    from
        user_feature features0_ 
    inner join
        feature feature1_ 
            on features0_.feature_id=feature1_.id 
    where
        features0_.user_id=?
Hibernate: 
    select
        features0_.user_id as user_id2_2_0_,
        features0_.feature_id as feature_1_2_0_,
        feature1_.id as id1_0_1_,
        feature1_.description as descript2_0_1_,
        feature1_.enabled as enabled3_0_1_,
        feature1_.feature_group as feature_4_0_1_,
        feature1_.name as name5_0_1_ 
    from
        user_feature features0_ 
    inner join
        feature feature1_ 
            on features0_.feature_id=feature1_.id 
    where
        features0_.user_id=?
Hibernate: 
    select
        features0_.user_id as user_id2_2_0_,
        features0_.feature_id as feature_1_2_0_,
        feature1_.id as id1_0_1_,
        feature1_.description as descript2_0_1_,
        feature1_.enabled as enabled3_0_1_,
        feature1_.feature_group as feature_4_0_1_,
        feature1_.name as name5_0_1_ 
    from
        user_feature features0_ 
    inner join
        feature feature1_ 
            on features0_.feature_id=feature1_.id 
    where
        features0_.user_id=?
Hibernate: 
    select
        features0_.user_id as user_id2_2_0_,
        features0_.feature_id as feature_1_2_0_,
        feature1_.id as id1_0_1_,
        feature1_.description as descript2_0_1_,
        feature1_.enabled as enabled3_0_1_,
        feature1_.feature_group as feature_4_0_1_,
        feature1_.name as name5_0_1_ 
    from
        user_feature features0_ 
    inner join
        feature feature1_ 
            on features0_.feature_id=feature1_.id 
    where
        features0_.user_id=?
Hibernate: 
    select
        features0_.user_id as user_id2_2_0_,
        features0_.feature_id as feature_1_2_0_,
        feature1_.id as id1_0_1_,
        feature1_.description as descript2_0_1_,
        feature1_.enabled as enabled3_0_1_,
        feature1_.feature_group as feature_4_0_1_,
        feature1_.name as name5_0_1_ 
    from
        user_feature features0_ 
    inner join
        feature feature1_ 
            on features0_.feature_id=feature1_.id 
    where
        features0_.user_id=?

Problem dalej nie występuje :(

1 użytkowników online, w tym zalogowanych: 0, gości: 1