인프런 커뮤니티 질문&답변

k-dev님의 프로필 이미지

작성한 질문수

실전! 스프링 데이터 JPA

@EntityGraph

fetch join 시 child table 조건이 실제 쿼리에 반영이 안되네요..

작성

·

641

0

[LneQuest]

@Entity(name = "LneQuest")

@Table(name = "lne_quests")

@Cacheable

@JsonInclude(JsonInclude.Include.NON_NULL)

//@NamedEntityGraph(name = "LneQuest.all", attributeNodes = [NamedAttributeNode("quizList")])

class LneQuest (

    var type: String,

    var title: String,

    var description: String?,

    @Column(name = "user_constraints")

    var userConstraints: String = "{}",

    @Column(name = "reward_total")

    var rewardTotal: Double,

    @Column(name = "reward_remain")

    var rewardRemain: Double,

    @Column(name = "reward_amount")

    var rewardAmount: Double,

    @Column(name = "reward_currency")

    var rewardCurrency: String,

    @Column(name = "thumbnail_url")

    var thumbnailUrl: String,

    @Column(name = "start_at")

    var startAt: LocalDateTime,

    @Column(name = "end_at")

    var endAt: LocalDateTime,

    @Column(name = "deleted_at")

    var deletedAt: LocalDateTime? = null,

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "quest" /* , cascade = [CascadeType.ALL] */)

    var quizList: MutableList<LneQuiz> = mutableListOf()

): AbstractJpaPersistable1() {

    // 연관관계 추가 method 는 만들어 놓는게 편리하다.

    fun addQuiz(quizz: LneQuiz) {

        quizz.quest = this

        quizList.add(quizz)

    }

}

@Entity(name = "LneQuiz")

@Table(name = "lne_quizzes")

@Cacheable

@JsonInclude(JsonInclude.Include.NON_NULL)

open class LneQuiz(

    @JsonIgnore

    @ManyToOne(fetch = FetchType.LAZY)

    @JoinColumn(name = "quest_id")

    var  quest: LneQuest,

    var  type: String,

    var  title: String,

    var  description: String? = null,

    var  answers: String? = null,

    @Column(name = "correct_answer")

    var  correctAnswer: String,

    @Column(name = "deleted_at")

    var  deletedAt: LocalDateTime? = null

): AbstractJpaPersistable() {

}

@Query("select q " +

            " from LneQuest q INNER JOIN FETCH LneQuiz qz where 1=1 " +

            " and (:type is null or qz.type = :type) " +

            " and (:from is null or :from < q.createdAt) " +

            " and (:to is null or q.createdAt < :to)", nativeQuery = false)

    fun findAllByJpql(@Param("type")type: String?,

                      @Param("from")from: LocalDateTime?,

                      @Param("to")to: LocalDateTime?, pageable: Pageable): Page<LneQuest>

                      

 페치 조인하니까 N+1 문제가 없이 쿼리 1번만 가는데요           조인되는 Child 테이블 컬럼 조건이 반영이 안되네요...;;;          

qz.type = :type  Child 테이블 컬럼 조건을 넣어도

페치 조인 쿼리의 경우 Main 테이블 조건으로 들어가네요...

컬럼명이 같은게 있어서 그런건지요???

                      

SELECT lnequest0_.id               AS id1_0_0_, 

       quizlist1_.id               AS id1_1_1_, 

       lnequest0_.created_at       AS created_2_0_0_, 

       lnequest0_.updated_at       AS updated_3_0_0_, 

       lnequest0_.deleted_at       AS deleted_4_0_0_, 

       lnequest0_.description      AS descript5_0_0_, 

       lnequest0_.end_at           AS end_at6_0_0_, 

       lnequest0_.reward_amount    AS reward_a7_0_0_, 

       lnequest0_.reward_currency  AS reward_c8_0_0_, 

       lnequest0_.reward_remain    AS reward_r9_0_0_, 

       lnequest0_.reward_total     AS reward_10_0_0_, 

       lnequest0_.start_at         AS start_a11_0_0_, 

       lnequest0_.thumbnail_url    AS thumbna12_0_0_, 

       lnequest0_.title            AS title13_0_0_, 

       lnequest0_.type             AS type14_0_0_, 

       lnequest0_.user_constraints AS user_co15_0_0_, 

       quizlist1_.created_at       AS created_2_1_1_, 

       quizlist1_.updated_at       AS updated_3_1_1_, 

       quizlist1_.answers          AS answers4_1_1_, 

       quizlist1_.correct_answer   AS correct_5_1_1_, 

       quizlist1_.deleted_at       AS deleted_6_1_1_, 

       quizlist1_.description      AS descript7_1_1_, 

       quizlist1_.quest_id         AS quest_i10_1_1_, 

       quizlist1_.title            AS title8_1_1_, 

       quizlist1_.type             AS type9_1_1_, 

       quizlist1_.quest_id         AS quest_i10_1_0__, 

       quizlist1_.id               AS id1_1_0__ 

FROM   korbit.lne_quests lnequest0_ 

       LEFT OUTER JOIN korbit.lne_quizzes quizlist1_ 

                    ON lnequest0_.id = quizlist1_.quest_id 

WHERE  lnequest0_.type = 'multiple-choice' 

       AND ( lnequest0_.created_at BETWEEN '2019-03-10T02:00:00.000+0000' AND 

                                               '2020-03-10T02:00:00.000+0000' );  

답변 6

2

k-dev님의 프로필 이미지
k-dev
질문자

• 자바진영의ORM기술표준 

• JPA를 왜 사용해야 하는가? 

• JPA동작-저장, 조회

• JPA와상속-저장,조회

• 성능최적화 - 1치 캐쉬

• 성능최적화 - Lazy 로딩 

• OSIV 최적화

• 양방향 연관관계 Owner

• QueryDsl 장점

• QueryDsl 쿼리 사용을 위한 Repository 구조 (Custom Repository, Impl, Querydsl4RepositorySupport)

• N+1 문제 해결

.  페이징 & 소팅

• Projections

JPA 관련 강의 5개를 들으면서 정리해 본 주제 인데요.

실무에서 중요한  팁(@OneToMany 에서 batch_size 설정을 통한 N+1 문제 해결, osiv on/off 이슈등) 또는

실무에서 발생할 수 있는 중요 포인트 중에 놓친게 없는지 한번 확인 부탁드려도 될런지요?

2

김영한님의 프로필 이미지
김영한
지식공유자

나누어서 답을 드릴께요^^

qz.type = :type

-> 저는 잘 되더라구요. 다음 코드를 참고해주세요.

참고로 일대다 fetch join에서 다 쪽을 이렇게 필터링 하는 것은 위험합니다. 다음 링크를 참고해주세요.

https://www.inflearn.com/questions/15876

@Entity
@Getter @Setter
public class Team {

    @Id @GeneratedValue
    private Long id;

    private String teamName;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

}

@Entity
@Getter
@Setter
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String memberName;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    public Team team;

}

public interface TeamRepository extends JpaRepository<Team, Long> {

    @Query("select t from Team t join fetch t.members m where m.memberName = :memberName")
    List<Team> findTeams(@Param("memberName") String memberName);
}

더불어 일반적인 @OneToMany 를 QueryDsl FetchJoin 으로 작성해 봤는데요

N+1 문제는 잘 해결되는데 페이징 처리는 안되서 혹시 방법이 있는지요?

-> 이 부분은 앞서 말씀드린 것 처럼 활용2편을 보셔야 합니다^^! 궁금해 하시는 모든 내용이 설명되어 있습니다. 활용2편을 차근차근 따라하시면 일대다 fetch join의 문제점과 해결 방안을 모두 이해하실 수 있습니다.

JPQL @Query 어노테이션도 Runtime 에 Syntax 체크를 해주긴 해주더라구요...

QueryDsl 의 경우에는 Syntax 체크가 언어 레벨에서 되는 점이 더 편할 것 같긴 합니다만,

복잡한 쿼리의 경우 QueryDsl 로 작성하는게 더 검증이나 유지보수가 더 편리할까요? (엔터프라이즈 상황에서요)

-> Runtime도 좋지만 컴파일 시점에 되는 것이 확실히 더 편리합니다. 그런데 Querydsl은 자바 코드여서 리펙토링도 가능하고 재사용 가능하도록 구조화도 가능해서 확실히 실무에서 더 좋습니다. 저도 제 주변 개발자 분들도 엔터프라이즈 상황에서 매우 열심히 사용하고 있습니다.

감사합니다.

0

김영한님의 프로필 이미지
김영한
지식공유자

네 잘 정리해주셨네요^^

우선은 이정도만 알고 들어가도 충분합니다. 나머지는 결국 실무에서 부딪히는 상황마다 조금씩 다른데, 5개 강의를 완강하셨으면 충분히 찾아서 극복하실 수 있을 거에요.

감사합니다.

0

k-dev님의 프로필 이미지
k-dev
질문자

데모용으로 사용하신 프로젝트를 공유해 주시면 제가 테스트 중인 코드를 좀 더 쉽게 공유할 수 있을 것 같습니다.

혹시 가능하실런지요?

0

k-dev님의 프로필 이미지
k-dev
질문자

@EntityGraph(attributePaths = ["quizList"])
@Query(value = "select q " +
" from LneQuest q INNER JOIN q.quizList qz where 1=1 " +
" and (:type is null or q.type = :type) " +
" and (:from is null or :from < q.createdAt) " +
" and (:to is null or q.createdAt < :to) ",
countQuery = "select count(q)" +
" from LneQuest q where 1=1 " +
" and (:type is null or q.type = :type) " +
" and (:from is null or :from < q.createdAt) " +
" and (:to is null or q.createdAt < :to)", nativeQuery = false)
fun findAllByJpql(@Param("type")type: String?,
@Param("from")from: LocalDateTime?,
@Param("to")to: LocalDateTime?, pageable: Pageable): Page<LneQuest>
val jpql =
selectFrom(lneQuest)
.innerJoin(lneQuest.quizList, lneQuiz)
.fetchJoin()
.where(
WhereBuilder()
.optionalAnd(from, { lneQuest.createdAt.after(from) })
.optionalAnd(to, { lneQuest.createdAt.before(to) })
.optionalAnd(type, { lneQuest.type.eq(type) })
)
.orderBy(*ordable(pageable.sort).toTypedArray())

val countJpql =
selectFrom(lneQuest)
.where(
WhereBuilder()
.optionalAnd(from, { lneQuest.createdAt.after(from) })
.optionalAnd(to, { lneQuest.createdAt.before(to) })
.optionalAnd(type, { lneQuest.type.eq(type) })
)

return applyPagination(pageable, jpql, countJpql)

안녕하세요 답변 주셔서 감사합니다.

문의 드리고 싶은 부분들을 정리해서 github 에 데모로 만들어서

이슈를 확인받고 싶은데, 잘 안되네요.... ;;;

qz.type = :type

으로 조건을 걸 수 있는 방법이 있다면  추가로 알려주시면 감사하겠습니다.

(Querydsl 또는 JPQL 어노테이션 쿼리 모두 상관없습니다)

-> 가능하면 저도 H2 db 로 테스트 코드 한번 만들어서 공유드리도록 하겠습니다.

더불어 일반적인 @OneToMany 를 QueryDsl FetchJoin 으로 작성해 봤는데요

N+1 문제는 잘 해결되는데 페이징 처리는 안되서 혹시 방법이 있는지요?

-> 아니면 FetchJoin 시에는 구조적으로 페이징 처리가 안되는지요?

.fetchJoin() 을 제거하면 N+1 문제가 발생하지만 페이징 처리와 Sort 는 모두 잘 됩니다.

JPQL @Query 어노테이션도 Runtime 에 Syntax 체크를 해주긴 해주더라구요...

QueryDsl 의 경우에는 Syntax 체크가 언어 레벨에서 되는 점이 더 편할 것 같긴 합니다만,

복잡한 쿼리의 경우 QueryDsl 로 작성하는게 더 검증이나 유지보수가 더 편리할까요? (엔터프라이즈 상황에서요)

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. kepha님^^

질문 여부를 떠나서 사실 이 코드는 정상 동작하지 않습니다.

JPA에서 OneToMany 관계를 fetch join 하면 페이징 처리가 불가능합니다. 관련해서 이런 경우 성능을 최적화 할 수 있는 방법을 활용2편에서 설명드립니다.

그리고 적어주신 문제는 추가로 파악해서 도움을 드리고 싶은데, 이 코드만으로는 정확한 이유를 찾기가 쉽지 않네요.

이 문제를 재현할 수 있는 간단한 자바 코드(코틀린X)와 테스트 케이스를 만들고 전체 프로젝트를 압축해서 올려주세요.

감사합니다.

k-dev님의 프로필 이미지

작성한 질문수

질문하기