Java/JPA

Spring Data JPA - join, 페이징 정리

blogger903 2024. 8. 21. 16:35
728x90

ToMany, ToOne 관계에서 Spring Data JPA로 join시 페이징 처리에 대한 정리입니다.

오늘 포스트는 Spring Data JPA에서는 ToOne, ToMany 관계의 join의 페이징 하는 방법이 다룹니다. 두 관계에 처리 방법이 다릅니다. 그리고 최적화 내용까지 다룹니다.

먼저, ToOne의 경우에는 fetchJoin으로 페이징쿼리를 하는게 어렵지 않습니다.
join시 row수가 증가하지 않기 때문입니다.

ToMany, OneToMany의 경우에는 (ManyToMany는 사용하지 않습니다.) 페이징 처리가 쉽지않습니다.

ToOne 관계는 fetchjoin으로 조회합니다.
ToMany 관계는 지연로딩으로 조회합니다. 지연로딩 최적화를 위해 hibernate.default_batch_size를 이용해서 N번 쿼리를 1번으로 줄입니다.

다음과 같이 application.yml에 설정해둘 수 있습니다.

spring:
  datasource:
    url: jdbc:h2:mem:study;  # In-memory H2 database
    username: sa
    password:
    driver-class-name: org.h2.Driver
  h2:
    console:
      enabled: true
      path: /h2-console
  jpa:
    hibernate:
      ddl-auto: create
#    show-sql: true
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true
        dialect: org.hibernate.dialect.H2Dialect
        default_batch_fetch_size: 100

toMany Repository 예시

1: N 관계의 경우 Lazy Fetch를 적용했기 때문에
1에 해당하는 엔티티를 DB에 조회해온후 N에 해당하는 필드를
직접 참조하는 상황에 쿼리가 나가게 됩니다.

그러면 1번의 쿼리에 속하는 필드 개수만큼 쿼리가 나갑니다.
이걸 hibernate.default_batch_size를 통해 in절로 배치처리 되어 1번만 조회하도록 개선됩니다.



@Component
public class FeedCustomRepository {

    private final JPAQueryFactory queryFactory;

    public FeedCustomRepository(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }

    public List<FeedDto> searchFeeds(FeedSearchRequest request) {
        QFeed feed = QFeed.feed;

        List<Feed> feedList = queryFactory
                .selectFrom(feed)
                .where(memberNameLike(request.getMemberName()))
                .orderBy(feed.id.desc())
                .offset(request.getOffset())
                .limit(request.getSize())
                .fetch();

        return feedList.stream().map(f -> {
            List<SMIDto> smiList = f.getStudyModuleInstanceList().stream().map(s -> {
                List<SUIDto> suiList = s.getStudyUnitInstanceList().stream().map(u -> {
                    List<SAIDto> saiList = u.getStudyActivityInstanceList().stream().map(a ->
                            new SAIDto(a.getId(), a.getAnswer(), a.getUserAnswer(), a.getIsCorrect())
                    ).collect(Collectors.toList());
                    return new SUIDto(u.getId(), u.getCompleteRate(), saiList);
                }).collect(Collectors.toList());
                return new SMIDto(s.getId(), s.getCompleteRate(), suiList);
            }).collect(Collectors.toList());
            return new FeedDto(f.getId(), smiList);
        }).collect(Collectors.toList());
    }

    public Optional<Feed> findById(Long id) {
        QFeed feed = QFeed.feed;

        Feed foundFeed = queryFactory.selectFrom(feed)
                .where(feed.id.eq(id))
                .fetchOne();

        // Null 체크를 Optional로 처리
        if (foundFeed == null) {
            return Optional.empty();
        }

        // 모든 수정을 수집한 후에 컬렉션에 추가
        List<StudyModuleInstance> collectedSMIs = foundFeed.getStudyModuleInstanceList().stream().map(smi -> {
            List<StudyUnitInstance> collectedSUIs = smi.getStudyUnitInstanceList().stream().map(sui -> {
                List<StudyActivityInstance> collectedSAIs = sui.getStudyActivityInstanceList().stream()
                        .toList();
                sui.getStudyActivityInstanceList().addAll(collectedSAIs);
                return sui;
            }).toList();
            smi.getStudyUnitInstanceList().addAll(collectedSUIs);
            return smi;
        }).toList();

        foundFeed.getStudyModuleInstanceList().addAll(collectedSMIs);

        return Optional.of(foundFeed);
    }

    private BooleanExpression memberNameLike(String name) {
        if (name == null || name.isEmpty()) {
            return null;
        }
        return QFeed.feed.member.name.startsWith(name);
    }
}

'Java > JPA' 카테고리의 다른 글

OneToOne 관계에서의 지연 로딩과 N+1 문제 심층 분석  (0) 2025.02.21
Enum - Converter  (0) 2024.07.31