인프런/자바 ORM 표준 JPA 프로그래밍 - 기본편

11] 객체지향 쿼리 언어 - 중급 문법

backend dev 2024. 6. 4.

경로 표현식

.(점)을 찍어 객체 그래프를 탐색하는 것

 

select m.username -> 상태 필드
 from Member m
 join m.team t -> 단일 값 연관 필드
 join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'

 

엔티티에서 바로 탐색가능한것 == 상태필드

상태필드로 객체 그래프를 탐색했다.라고 볼수있다.

 

3가지 경로표현식이 존재

 

경로 표현식 용어 정리

상태 필드(state field): 단순히 값을 저장하기 위한 필드 (ex: m.username)

 

 

연관 필드(association field): 연관관계를 위한 필드

 

단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team)

 

컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders)

 

 

 

경로 표현식 특징

상태 필드(state field): 경로 탐색의 끝이므로 더이상 탐색이 안된다. [ 객체가 아니니까 .을 통해 더이상 이동할곳이없음]

[ ex) m.username 이후 .을통해 더 탐색 할 곳이 없다.]

 

단일 값 연관 경로: 묵시적 내부 조인(inner join) 발생, 탐색 가능

[ ex) m.team.name  과 같이 m.team 이후로 탐색할곳이 존재함]

select m.team from Member m 이렇게 JPQL를 생성해도 실제 쿼리는

member테이블과 team 테이블이 join하는 쿼리가 생성되고 실행된다. == > 묵시적 내부 조인

[ 실무에서는 묵시적 내부 조인이 발생하도록 쿼리를 만들면 안된다. 성능 튜닝이 어렵기 때문이다. ]

 

컬렉션 값 연관 경로: 묵시적 내부 조인 발생, 탐색X

    - FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능

 

[ ex) t.members 하면 list일텐데 거기서 . 으로 더이상 탐색 불가 t.members.size 까지정도만 가능 

(반환된 리스트의 크기정도)]

 

FROM 절에서 명시적 조인을 통해 별칭얻어서 탐색 예시

ex) select m.username from Team t join t.members  m

• select t.members.username from Team t -> 실패

• select m.username from Team t join t.members m -> 성공

 

결론 : 실무에서 묵시적 조인 사용 X , 명시적 조인을 사용해라

 

명시직 조인, 묵시적 조인

명시적 조인: join 키워드 직접 사용

• select m from Member m join m.team t



묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부 조인만 가능)

• select m.team from Member m

 

 

경로 탐색을 사용한 묵시적 조인 시 주의사항

• 묵시적 조인은 항상 내부 조인

 

• 컬렉션은 경로 탐색의 끝이므로 명시적 조인을 통해 별칭을 얻어야만 탐색을 이어 나갈 수 있다.

 

• 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 줌

[자동으로 join쿼리가 생성되니까]

 

 

실무 조언

• 가급적 묵시적 조인 대신에 명시적 조인 사용

 

• 조인은 SQL 튜닝에 중요 포인트이므로 명시적 조인 사용

 

묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움

 


 

JPQL - 페치 조인(fetch join)

 

• SQL 조인 종류가 아니다.

 

• JPQL에서 성능 최적화를 위해 제공하는 기능

 

• 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회하는 기능

 

• join fetch 명령어 사용

 

• 페치 조인 ::=

 [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로

 

엔티티 페치 조인

• 회원을 조회하면서 연관된 팀도 함께 조회(SQL 한번에)

 

• SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT

 

[JPQL]
select m from Member m join fetch m.team
[SQL]
SELECT M.*, T.* FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID

select m 해서 m의 정보만 조회했지만

실제 쿼리에는 t.*까지 추가된것을 확인할 수 있다.

 

즉시로딩처럼 원하는 객체를 한번에 조회할때 페치 조인을 사용한다.

inner join이므로 회원4는 없고,

회원1,2,3 팀A,B가 조회되어 1차캐시에서 관리된다.

 

페치 조인 사용 코드

String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
        .getResultList();
for (Member member : members) {
    //페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩X
    System.out.println("username = " + member.getUsername() + ", " +
            "teamName = " + member.getTeam().name());
}

fetch join으로 인해 회원을 조회할때 Team까지 조회되서 지연로딩이 적용되지않는다.[ 지연로딩무시]

즉 한번에 join해서 가져오므로 프록시객체가 들어가지않고, 실제 엔티티값이 들어가있다.

 

LAZY,EAGER 둘다 N + 1 문제가 발생하는데 

[한번의 쿼리 이후에 추가적인 쿼리가 발생하는 문제]

 

페치 조인으로 N + 1 문제 해결가능

 

fetch 조인, 엔티티 그래프 질문입니다. - 인프런

안녕하세요. 강의를 들으며 조금 불분명한 부분이 있어서 질문 남깁니다. 가령 멤버들의 팀을 조회하는 쿼리를 작성한다면 우선 팀 전체를 조회하는 쿼리가 실행되고 각 팀이 자신들의 멤버를

www.inflearn.com

 

em.find로 조회한다면 즉시로딩으로 설정되있을경우 join으로 한번에 쿼리를 생성하여 가져오겠지만

 

JPQL을 사용한다면 작성한 SQL을 번역해서 조회를 진행한다.

위의 예시대로 진행시 멤버만 조회를 하고, 즉시 로딩이 설정된 멤버,팀 각각 쿼리를 따로 날려서 추가 조회가 된다. ( N+1 문제)

 

즉 한번에 join을 통해 쿼리가 날아가지않는다.

 

[em.find로 조회한다면 엔티티에 설정한 fetch Type에 따라 sql이 다르겠지만
jpql을 사용한다면  fetch Type은 무시되고 작성한 jpql에 따라 생성되는 sql이 다르다.]

JPQL에서 fetch join을 사용하면 join을 통해 한번에 가져온다.

 

em.find를 사용하고 즉시로딩을 사용했을때 결과처럼.

 

 

컬렉션 페치 조인

일대다 관계, 컬렉션 페치 조인

[JPQL]
select t
from Team t join fetch t.members
where t.name = ‘팀A'

[SQL]
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

팀A는 회원1,2를 가지고있다.

 

같은 팀이지만 해당 회원이 2명이므로 조회 결과는 2개가 나온다.

 

그러므로 JPQL의 조회결과는 같은 팀A가 2번 나오게된다.

 

이렇게 일대다 관계에서는 데이터가 생각보다 많이나올 경우가 존재한다.

String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
for(Team team : teams) {
    System.out.println("teamname = " + team.getName() + ", team = " + team);
    for (Member member : team.getMembers()) {
        //페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
        System.out.println(“-> username = " + member.getUsername()+ ", member = " + member);
    }
}

위의 코드의 결과, 팀A가 2번출력된다 [ 팀A에 회원이 2명있기 떄문, join의 결과로 데이터가 2개이기 때문]

 

 

 

페치 조인과 DISTINCT

 

 

SQL의 DISTINCT는 중복된 결과를 제거하는 명령

 

JPQL의 DISTINCT 2가지 기능 제공

• 1. SQL에 DISTINCT를 추가

• 2. 애플리케이션에서 엔티티 중복 제거

select distinct t
from Team t join fetch t.members
where t.name = ‘팀A’

SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과에서 중복제거 실패

엔티티 중복제거하는 기능까지 필요하다.

JPQL에 distinct를 추가하면 jpa는 sql에 추가까지하고, 엔티티 중복도 제거해준다.

회원1, 회원2 각각 다르니까 SQL의 DISTINCT로는 중복 제거 불가. 그러므로 jpa는 엔티티중복제거 기능까지 진행한다.

 

 

• DISTINCT가 추가로 애플리케이션에서 중복 제거시도

 

같은 식별자를 가진 Team 엔티티 제거

0x100 가 식별자

 

 

하이버네이트6 변경 사항

DISTINCT가 추가로 애플리케이션에서 중복 제거시도 

-> 하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용됩니다.  

 

저만 이상하게 되나요? 21:36 join fetch 해도 2번만 나오는 이유가.... - 인프런

[질문 템플릿]1. 강의 내용과 관련된 질문인가요? 예2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? 예3. 질문 잘하기 메뉴얼을 읽어보셨나요? 예[질문 내용]@ManyToOne(fetch = FetchType.L

www.inflearn.com

 

 

 

페치 조인과 일반 조인의 차이

일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음

 

[JPQL]
select t
from Team t join t.members m
where t.name = ‘팀A'

[SQL]
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

 

즉 m.*를 안한다는것 [ 멤버는 조인만 진행하고, 데이터는 Team에 대한것만 가져온다. 셀렉트절에 있는것만 가져온다.

그러므로 멤버를 사용하려고할때 다시 한번 쿼리가 나가야한다.]

 

페치 조인이였다면

t.* , m.* 으로 SQL 결과가 나왔을것이다.

 

• JPQL은 결과를 반환할 때 연관관계 고려하지않고 select 절에만 있는것을 가져온다. ( t 만있으니까 t.*로 )

 

단지 SELECT 절에 지정한 엔티티만 조회할 뿐

 

여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회X

 

페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩이 일어난다고 보면된다.)

 

• 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

 

 

페치 조인의 특징과 한계

 

페치 조인 대상에는 별칭을 줄 수 없다.

     • 하이버네이트는 가능, 가급적 사용X

select t
from Team t join fetch t.members as m // 사용 X

둘 이상의 컬렉션은 페치 조인 할 수 없다.

join fetch t.members join fetch t.orders 처럼 2개 이상 컬렉션 페치 조인 X

하나의 컬렉션만 fetch join해도 데이터가 늘어나서 distinct 기능을 사용해야하는데

2개이상을 한다면 조회되는 데이터가 생각과 다를 수 있다.

 

컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.

일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능

[ 다대다,일대다 처럼 데이터가 생각지도 못하게 늘어나지않으니까 ]

[ 다대다 ,일대다 같은 경우  페이징 처리시 데이터가 짤려서 예상과 다르게 동작할 수 있다.

페이징은 데이터베이스상에서 로우수를 가지고 처리하는것인데 다대다,일대다의 경우 로우수가 불어나기 때문에

페이징 처리가 불가능하다. JPA가 엔티티 중복을 처리해주는것은 DB의 결과를 가지고하는것이니까]

 

하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)

[ 데이터베이스에서 로우수를 처리할 수 없으므로 하이버네이트가 모든 조회결과를 가져와서 메모리에서 페이징 처리를 해주지만 매우 위험하다. ]

 

 

해결방법 

1.

select t
from Team t join fetch t.members

는 일대다 이므로 

뒤집어서

select m
from Member m join fetch m.team

다대일로 쿼리를 만들어서 페이징 처리한다.

 

 

2.

배치 사이즈 사용 - 다시 듣기 

 

 

할수 있다면 다대일로 만들어서 처리하는것이 성능이 좋다.

 

 

 

 

연관된 엔티티들을 SQL 한번으로 조회 - 성능 최적화

 

• 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함

@OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략  다음과 같이 설정하는 글로벌 로딩 전략보다
fetch join이 우선권을 가진다. 그러므로 LAZY로 설정해놓아도 fetch join 으로 JPQL을 짰다면
join으로 쿼리가 생성되어 실행된다.

 

• 실무에서 글로벌 로딩 전략[fetch~으로 속성설정하는거]은 모두 지연 로딩

 

• 최적화가 필요한 곳은 페치 조인 적용

 

페치 조인 - 정리

 

• 모든 것을 페치 조인으로 해결할 수 는 없음

 

• 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적

 

여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면,

페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적




JPQL - 다형성 쿼리

 

 

• 조회 대상을 특정 자식으로 한정

 

• 예) Item 중에 Book, Movie를 조회해라

 

[JPQL]
select i from Item i
where type(i) IN (Book, Movie)

[SQL]
select i from i
where i.DTYPE in (‘B’, ‘M’)

위와 같은 JPQL로 특정 자식 조회가능

 

 

TREAT(JPA 2.1)

 

  자바의 타입 캐스팅과 유사

 

• 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용

 

• FROM, WHERE, SELECT(하이버네이트 지원) 사용

 

 

예) 부모인 Item과 자식 Book이 있다.

 

[JPQL]
select i from Item i
where treat(i as Book).author = ‘kim’

[SQL]
select i.* from Item i
where i.DTYPE = ‘B’ and i.author = ‘kim’

 

 

 

 


JPQL - 엔티티 직접 사용

 

 

엔티티 직접 사용 - 기본 키 값

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본키 값을 사용

 

[JPQL]
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용

[SQL](JPQL 둘다 같은 다음 SQL 실행)
select count(m.id) as cnt from Member m

 

 

// 엔티티를 파라미터로 전달
String jpql = “select m from Member m where m = :member”;
List resultList = em.createQuery(jpql)
 .setParameter("member", member)
 .getResultList(); 
 
 
// 식별자를 직접 전달
String jpql = “select m from Member m where m.id = :memberId”;
List resultList = em.createQuery(jpql)
 .setParameter("memberId", memberId)
 .getResultList();

 

실행된 SQL

select m.* from Member m where m.id=?

 

엔티티 직접 사용 - 외래 키 값

 // 엔티티를 파라미터로 전달
Team team = em.find(Team.class, 1L);
String qlString = “select m from Member m where m.team = :team”;
List resultList = em.createQuery(qlString)
 .setParameter("team", team)
 .getResultList(); 
 
 
 // 식별자를 직접 전달
String qlString = “select m from Member m where m.team.id = :teamId”;
List resultList = em.createQuery(qlString)
 .setParameter("teamId", teamId)
 .getResultList();

 

실행된 SQL

select m.* from Member m where m.team_id=?

 

 

 

JPQL - Named 쿼리

 
 

Named 쿼리 - 정적 쿼리

• 미리 정의해서 이름을 부여해두고 사용하는 JPQL

 

• 정적 쿼리에만 사용가능

 

• 어노테이션, XML에 정의

 

애플리케이션 로딩 시점에 초기화 후 재사용

[로딩시점에 SQL로 parsing해서 캐시로 가지고있는다 ]

 

애플리케이션 로딩 시점에 쿼리를 검증

 

Named 쿼리 - 어노테이션

@Entity
       @NamedQuery(
               name = "Member.findByUsername",
               query="select m from Member m where m.username = :username")
       public class Member {
...
       }
List<Member> resultList =
        em.createNamedQuery("Member.findByUsername", Member.class)
                .setParameter("username","회원1")
                .getResultList();

정의 해둔 name을 이용하여 쿼리를 사용할 수 있다.

 

Named 쿼리 - XML에 정의

 

다음과 같이 정의해두고

List<Member> resultList =
        em.createNamedQuery("Member.findByUsername", Member.class)
                .setParameter("username","회원1")
                .getResultList();

가져와 사용할 수 있다.

 

 

Spring Data Jpa를 사용하면 @Query라는 어노테이션으로 더욱 간단하게 Named쿼리를 사용할 수 있다.

[이름은 설정 하지않고, sql로 파싱해서 캐시로 가지고있는 부분이 동일 ]

[ 실무에서는 Spring data jpa를 사용하므로 named쿼리를 사용할 일 이 없다. ]


JPQL - 벌크 연산

• 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?

 

• JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행

 

1. 재고가 10개 미만인 상품을 리스트로 조회한다.

 

2. 상품 엔티티의 가격을 10% 증가한다.

 

3. 트랜잭션 커밋 시점에 변경감지가 동작한다.

 

• 변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행

 

벌크 연산 예제

쿼리 한 번으로 여러 테이블 로우 변경(엔티티)

 

executeUpdate()의 결과는 영향받은 엔티티 수 반환

 

UPDATE, DELETE 지원

 

• INSERT(insert into .. select, 하이버네이트 지원)

 

String qlString = "update Product p " +
                "set p.price = p.price * 1.1 " +
                "where p.stockAmount < :stockAmount";
                               
int resultCount = em.createQuery(qlString)
                .setParameter("stockAmount", 10)
                .executeUpdate();

 

 

 

벌크 연산 주의

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리

[ 영속상태 엔티티와 DB의 데이터값이 다를 수 있으므로  아래 두가지 방법중 하나를 골라 사용 ]

    -  영속성컨텍스트를 사용하는 연산 전에 벌크 연산을 먼저 실행하거나

    -  벌크 연산 수행 후 영속성 컨텍스트 초기화해야한다. [벌크연산은 직접 DB에 쿼리를 날리는것이므로 영속성컨테이너에 있는 엔티티에는 변경이 반영이 되지않는다. 그러므로 영속성 컨테이너를 초기화하여 , 새로 조회할때 다시 DB에서 가져오도록 한다.]

 

 

[flush는 커밋할때,쿼리가 나갈때,flush()를 실행할때 진행된다. ]

 

Spring data jpa를 사용하면 @Query를 이용하여 벌크연산을 진행하고 @Modifying 과 같은 어노테이션을 추가해서

영속성컨테이너를 비우는 작업을 진행할수있다.

 

댓글