13 Kasım 2023 Pazartesi

JPA JPQL LEFT JOIN FETCH ve MultipleBagFetchException

Giriş
LEFT JOIN FETCH özellikle parent nesnede iki tane ilişki varsa problem olabiliyor. 

1. Set Döndürmek
Hibernate tarafından beklenen şey Set döndürmek. Set kullanırsak bile 
1. Full Cartesian Product problemi ortaya çıkar. 
2. Sayfalama çalışmıyor

Örnek-  Sayfalama Hatası
Elimizde şöyle bir kod olsun
@Entity
@Data
@NoArgsConstructor
@Table(name = "teams")
public class Team {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToMany(mappedBy = "team", 
    cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true)
  private Set<Member> members;

  @OneToMany(mappedBy = "team", 
    cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true)
  private Set<Milestone> milestones;

  // constructors, helper functions, etc ..
}

public interface TeamRepository extends JpaRepository<Team, Long> {

  @EntityGraph(attributePaths = {
            "members", "milestones"
  })
  Page<Team> findAll(Pageable pageable);
}
Açıklaması şöyle
When lookin at the response, it might lead you to believe that pagination is automatically handled when using the entity graph by passing in a Pageable to your query. However, it will give you a warning indicating that pagination is being performed in memory.
Çıktı şöyle
WARN 88609 --- [nio-8080-exec-3] org.hibernate.orm.query : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
2. List Döndürmek
Bazen kodda ilişkiyi temsil etmek için Set yerine List kullanılıyor. Bu durumda 
org.hibernate.loader.MultipleBagFetchException diye bir exception alırız.
Açıklaması şöyle
The reason why a MultipleBagFetchException is thrown by Hibernate is that duplicates can occur, and the unordered List, which is called a bag in Hibernate terminology, is not supposed to remove duplicates.
Çözüm olarak 2 tane JPQL kullanılır. Açıklaması şöyle
This can be solved if you use Set instead of List. If you really want to stick with List, then you would have to divide into two queries eagerly loading each child and merge in the application code. Therefore, it may require you to change some code if you need to load multiple child entities and the decision will depend on the feature acceptance criteria.

Örnek
Elimizde Post sınıfı için şöyle bir kod olsun. Burada PostComment ve Tag ilişkileri için List kullanılıyor.
@OneToMany(
    mappedBy = "post",
    cascade = CascadeType.ALL,
    orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();
 
@ManyToMany(
    cascade = {
        CascadeType.PERSIST,
        CascadeType.MERGE
    }
)
@JoinTable(
    name = "post_tag",
    joinColumns = @JoinColumn(name = "post_id"),
    inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private List<Tag> tags = new ArrayList<>();
Post nesnelerini çekmek için şöyle bir kod çalıştıralım. MultipleBagFetchException alırız
List<Post> posts = entityManager.createQuery("""
    select p
    from Post p
    left join fetch p.comments
    left join fetch p.tags
    where p.id between :minId and :maxId
    """, Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.getResultList();
Çözüm olarak 2 tane JPQL kullanılır. Şöyle yaparız
List<Post> posts = entityManager.createQuery("""
    select distinct p
    from Post p
    left join fetch p.comments
    where p.id between :minId and :maxId""", Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();
 
posts = entityManager.createQuery("""
    select distinct p
    from Post p
    left join fetch p.tags t
    where p in :posts""", Post.class)
.setParameter("posts", posts)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();
Örnek
Elimizde şöyle bir kod olsun
public class Lesson {
  @Id
  private Long id;
    // ...other properties
  @OneToMany(mappedBy = "lesson", cascade = CascadeType.ALL)
  private List<Student> students;
  @OneToMany(mappedBy = "lesson", , cascade = CascadeType.ALL)
  private List<Guest> guests;
    // ...constructors, getters and setters
}
Yine 2 tane JPQL kullanılır. Şöyle yaparız
@Repository
public class LessonCriteriaRepositoryImpl implements LessonCriteriaRepository {

  @PersistenceContext
  private EntityManager entityManager;

  public List<Lesson> findAll() {
    //build first query for fetching students
    CriteriaBuilder builder = entityManager.getCriteriaBuilder();
    CriteriaQuery<Lesson> criteriaQuery = builder.createQuery(Lesson.class);
    Root<Lesson> lesson = criteriaQuery.from(Lesson.class);
    lesson.fetch("students", JoinType.LEFT);
    criteriaQuery.select(lesson).distinct(true);
    TypedQuery<Lesson> query1 = entityManager.createQuery(criteriaQuery);
    List<Lesson> lessons = query1.getResultList();

    //build second query for fetching guests
    builder = entityManager.getCriteriaBuilder();
    criteriaQuery = builder.createQuery(Lesson.class);
    lesson = criteriaQuery.from(Lesson.class);
    lesson.fetch("guests", JoinType.LEFT);
    criteriaQuery.select(lesson).distinct(true).where(lesson.in(lessons));
    TypedQuery<Lesson> query2 = entityManager.createQuery(criteriaQuery);
    return query2.getResultList();
  }
}



Hiç yorum yok:

Yorum Gönder