JPA - JPQL 조인(객체지향쿼리),Java Persistence Query Language

2021. 4. 17. 01:55 Spring Data/Spring Data JPA

JPA - JPQL 조인(객체지향쿼리),Java Persistence Query Language 

 

JPQL 조인은 SQL 조인과 기능은 거의 같고 문법만 약간 다르다.

 

내부 조인(inner join)

/*
 * 내부조인
 */
public void innerJoin() {
    String jpql = "select m,t "
            + "from MemberJPQL m inner join m.team t "
            + "where t.name = '티스토리1' "
            ;
 
    Query query = em.createQuery(jpql);
    List members = query.getResultList();
 
    System.out.println("================innerJoin=================");
 
    for(Object o : members) {
        Object[] result = (Object[]) o;
        for(Object o2 : result) {
            System.out.print("result element => "+o2);
        }
        System.out.println();
    }
 
    System.out.println("================innerJoin=================");
 
}

 

 

JPQL 내부 조인 구문을 보면 SQL 문법과는 약간 다르다. SQL에서는 조인문에 연관테이블의 외래키로 직접 조인을 하지만 JPQL에서는 from 절의 엔티티의 연관필드로 조인하게 되어 있다. 만약 SQL처럼 조인하면 안된다. (ex ~ FROM MemberJPQL m JOIN TeamJPQL t --->오류발생)

 

그리고 select 다음에 만약 조인한 엔티티의 특정필드만 뽑는다면 위와 같이 꼭 조인할 연관엔티티에 별칭을 넣어줘야한다.

 

외부조인(outer join)

/*
 * 외부조인
 */
public void outerJoin() {
    String jpql = "select m,t "
            + "from MemberJPQL m left join m.team t "
            + "where t.name = '티스토리1' "
            ;
 
    Query query = em.createQuery(jpql);
    List members = query.getResultList();
 
    System.out.println("================outerJoin=================");
 
    for(Object o : members) {
        Object[] result = (Object[]) o;
        for(Object o2 : result) {
            System.out.print("result element => "+o2);
        }
 
        System.out.println();
    }
 
    System.out.println("================outerJoin=================");
 
}

컬렉션 조인(Collection Join)

/*
 * 컬렉션조인 - 일대다,다대다 관계처럼 필드에 컬렉션을 사용하는 곳에 조인하는 것.
 * 
 * ex) Team - > Member 일대다관계
 * 즉, 컬렉션을 연관필드로 조인하는 것이다.
 */
public void collectionJoin() {
    String jpql = "select t,m "
            + "from TeamJPQL t join t.members m "
            + "where t.name = '티스토리1' "
            ;
 
    Query query = em.createQuery(jpql);
    List members = query.getResultList();
 
    System.out.println("================collectionJoin=================");
 
    for(Object o : members) {
        Object[] result = (Object[]) o;
        for(Object o2 : result) {
            System.out.print("result element => "+o2);
        }
 
        System.out.println();
    }
 
    System.out.println("================collectionJoin=================");
 
}

 

 

위 주석에도 달려있지만 일대다, 다대다 관계처럼 연관필드에 컬렉션으로 정의되어있는 엔티티를 조인하는 방법이다.

 

 

세타조인(Theta Join)

 

WHERE 절을 사용해서 세타조인을 할 수 있다. SQL에서도 그렇듯이 세타 조인은 내부 조인만 가능하다. 그리고 위에서 했던 조인들은 연관된 엔티티필드를 이용해 조인을 했지만, 세타 조인은 전혀 관계없는 엔티티도 조인할 수 있다. 여기서 전혀 관계가 없다는 것은 매핑된 외래키로 조인하는 것이 아닌 일반 필드로 조인이 가능하다는 뜻이다.

 

/*
 * 세타조인 - where절을 이용한 조인
 *           - 내부조인만 지원하며, 연관필드를 가지지 않는 전혀 관계없는 엔티티도 조인해 결과로 리턴할 수 있다.
 */
public void thetaJoin() {
    String jpql = "select t,m "
            + "from TeamJPQL t ,MemberJPQL m "
            + "where m.team.name = t.name and t.name = '티스토리1'"
            ;
 
    Query query = em.createQuery(jpql);
    List members = query.getResultList();
 
    System.out.println("================thetaJoin=================");
 
    for(Object o : members) {
        Object[] result = (Object[]) o;
        for(Object o2 : result) {
            System.out.print("result element => "+o2);
        }
        System.out.println();
    }
 
    System.out.println("================thetaJoin=================");
 
}

 

Join on 절

 

JPA 2.1 버전부터 지원하는 기능이며, On절을 사용하면 조인 대상을 필터링하고 조인할 수 있다. 참고로 내부조인의 On절은 where절에서 필터링하는 결과와 같음으로 보통 On절은 외부 조인에서 사용한다.

 

/*
 * join on - on 절에서 조인 대상을 필터링한다. 내부조인의 on절은 where절과 같음으로
 *              보통 외부조인에서 사용한다. 
 */
public void joinOn() {
    String jpql = "select m "
            + "from MemberJPQL m left join m.team t on m.username = '여성게1' "
            ;
 
    TypedQuery<MemberJPQL> query = em.createQuery(jpql,MemberJPQL.class);
    List<MemberJPQL> members = query.getResultList();
 
    System.out.println("================joinOn=================");
 
    for(MemberJPQL m : members) {
        System.out.println(m.toString());
    }
 
    System.out.println("================joinOn=================");
 
}

 

패치조인(fetch Join)

 

패치조인은 SQL에 존재하는 조인의 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 이것은 연관된 엔티티나 컬렉션을 한번에 같이 조회하는 기능이다.

 

패치조인 문법 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로 ([]는 선택사항이다)

 

/*
 * fetch join - 프로젝션의 엔티티말고도 해당 엔티티의 연관된 모든 엔티티를 모두 조회한다.
 * 만약 회원을 지연로딩 설정하였어도 페치조인이기 때문에 실제엔티티 객체를 set한다.(그냥 조인은 MemberJPQL엔티티를 프록시나 
아직 초기화되기전인 컬렉션레퍼를 반환)
 * 
 * 주의사항 - 만약 티스토리1 이라는 팀에 연관된 멤버수가 n명이다라고 가정하면,
 * 데이터베이스에서는 총 n개의 로우가 결과로 생기게 된다. 즉, 영속성컨텍스트에도 
 * 결과리스트로 n개의 티스토리1 팀 엔티티가 리턴된다. 이러면 메모리 낭비일텐데....
 * 
 * 티스토리1 엔티티 -> 회원1,회원2
 * 티스토리1 엔티티 -> 회원1,회원2
 * 
 * 이럴때는 select distinct t from TeamJPQL t join fetch t.members where t.name='티스토리1'
 * 로 조회하면 된다. distinct는 2가지의 역할을 한다. 데이터베이스에 쿼리를 날릴때의 키워드로 붙고 나머지 하나는
 * 애플리케이션 단에서 영속성엔티티에게 중복결과를 제거하라는 명령이다. 이말은 즉슨, 위와같은 상황에서 데이터베이스는
 * 각 로우데이터가 다르기 때문에 영향이 없지만, 영속성컨텍스트에서는 티스토리1이라는 중복되는 엔티티들의 결과가 생기기때문에
 * 중복을 제거하여 List결과를 내보내준다.
 * 
 * 티스토리1 엔티티 -> 회원1,회원2
 * 티스토리1 엔티티 -> 회원1,회원2(x)
 * 
 * 즉, FetchType.LAZY로 설정하고 필요할때 fetch조인을 이용해 즉시조회하는 것이 애플리케이션단에 무리를 덜주게된다.
 * 
 */
public void fetchJoin() {
    //컬렉션 그래프 탐색을 할때는 반드시 별칭이 있어야한다.
    //별칭이 없다면 t.members까지가 최대 탐색이다.
    String jpql = "select t from TeamJPQL t join fetch t.members";
    
    TypedQuery<TeamJPQL> query = em.createQuery(jpql,TeamJPQL.class);
    List<TeamJPQL> teams = query.getResultList();
 
    System.out.println("================fetchJoin=================");
 
    for(TeamJPQL m : teams) {
        System.out.println(m.toString());
    }
 
    System.out.println("================fetchJoin=================");
 
}

 

JPQL문을 보면 t.members로 패치조인한 것이 눈에 보일 것이다. 다른 엔티티를 조인한 것이 아니고 자기자신의 연관필드로 조인을 한것이다. 그리고 패치조인에서는 조인대상이 되는 연관필드에 별칭을 넣을 수 없다. 즉, 사용이 불가한 것이다.

 

위의 JPQL은 밑의 SQL로 실행된다.

 

=>

 

SELECT

M.*,T.*

FROM TEAM_JPQL T

INNER JOIN MEMBER_JPQL M ON M.TEAM_ID=T.ID

 

팀엔티티는 프로젝션에 포함되지 않았는데, SQL은 팀엔티티까지 조회하게 된다.

 

위의 주석에도 설명했지만, 회원을 조회할 때 패치조인을 이용하여 팀엔티티를 같이 조회했을 경우에는 아무리 팀엔티티가 지연로딩(FetchType.LAZY)여도 프록시로 반환되는 것이 아니라 실제 엔티티가 반환되고 해당 엔티티가 영속성컨텍스트에 저장된다는 점이다.

 

 

컬렉션 패치조인 (Collection fetch Join)의 문제점과 해결방법

 

위에서 패치조인을 이용하여 팀엔티티의 연관 컬렉션인 회원엔티티를 한번에 조회했다. 하지만 이 조회에는 조금 문제가 있다.

 

 

위 그림과 같이 데이터베이스는 같은 팀테이블이지만 데이터가 다른 2개의 로우를 반환하고 있고, 그에 따라 JPA도 2개의 팀엔티티를 결과 리스트로 반환한다. 여기서 생각해보면 팀 테이블은 로우의 데이터가 다르니 2개의 반환값을 갖는 것이 이해되지만 애플리케이션 단의 JPA에서는 같은 팀 엔티티 객체를 꼭 2개를 반환할 필요는 없을 것 같다. 여기서 DISTINCT를 사용한다.

 

public void fetchJoin() {
    String jpql = "select distinct t from TeamJPQL t join fetch t.members where t.name='티스토리1'";
 
    TypedQuery<TeamJPQL> query = em.createQuery(jpql,TeamJPQL.class);
    List<TeamJPQL> teams = query.getResultList();
 
    System.out.println("================fetchJoin=================");
 
    for(TeamJPQL m : teams) {
        System.out.println(m.toString());
    }
 
    System.out.println("================fetchJoin=================");
 
}

바뀐 것은 select 다음에 distinct를 넣어준 것이다. 이 명령어가 하는 역할은 SQL에 DISTINCT명령어를 추가하는 것은 물론이고 애플리케이션단에서 한번 더 중복을 제거한다. 하지만 이 예제에서 SQL에 DISTINCT명령어가 추가되더라도 달라질 것은 없다. 왜냐? 두개의 로우의 데이터가 다르기 때문이다. 하지만 애플리케이션단에서는 다르다 중복된 팀엔티티 결과를 하나로 줄여주는 역할을 한다.

 

 

 

마지막으로 패치조인의 사용전략이다. 보통 애플리케이션에서 최적화를 위해 글로벌 로딩 전략으로 즉시 로딩(FetchType.EAGER)으로 설정하면 애플리케이션 전체에 불필요한 즉시로딩이 일어난다. 물론 일부는 빠를수도 있지만 전체적으로 봤을 경우엔 성능에 최악이다. 그래서 글로벌 로딩 전략으로는 지연로딩으로 설정하고 필요할때 패치조인을 이용한다면 애플리케이션 성능에 있어 훨씬 효과적일 것이다.

 

패치조인 특징

 

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

2. 둘이상의 컬렉션을 패치조인 할 수 없다.

3. 컬렉션을 패치 조인하면 페이징 API를 사용할 수 없다.

 

 

경로표현식

 

쉽게 말하면 쿼리에서 .(점)을 찍어 객체 그래프를 탐색하는 것이다.

ex) m.username, m.team, t.name ....

 

<경로표현식 용어 정리>

상태필드(state field) 

단순히 값을 저장하기 위한 필드(스칼라타입이되는 필드라 생각하면될듯) 

연관 필드(association field) 

연관관계를 위한 필드, 임베디드 타입 필드도 포함한다.

-단일 값 연관필드 : @ManyToOne,@OneToOne

-컬렉션 값 연관필드: @OneToMany,@ManyToMany 

 

 

 

<경로표현식 특징>

상태필드경로 

경로탐색의 끝이다. 더는 탐색할 수 없다. 

단일 값 연관 경로 

묵시적으로 내부조인이 일어난다. 계속해서 탐색가능하다. 

컬렉션 값 연관 경로 

묵시적으로 내부조인이 일어난다. 위와는 다르게 더이상 탐색불가하다. 단 FROM절에서 조인을 통해 별칭을 얻는다면 계속해서 탐색가능하다. 

 

/*
 * 묵시적 조인이다. 
 * 명시적으로 join문을 넣지 않아도 내부적으로 경로탐색(t.members)을 통하여 조인을한다.
 * t.membser.size에서 size는 하나의 키워드(참조가 아니다.)이고, 내부적으로 sql에 count함수로 들어간다.
 * 만약 밑처럼 members라는 컬렉션의 경로탐색을 더하고 싶으면 명시적인 조인으로 컬렉션 연관필드에 별칭을 줘야한다.
 */
public void ImplicitJoin2() {
    String jpql = "select t.members.size from TeamJPQL t where t.name = '티스토리1'";
    
    TypedQuery<Integer> query = em.createQuery(jpql,Integer.class);
    List<Integer> teams = query.getResultList();
 
    System.out.println("================Implicit=================");
 
    for(Object m : teams) {
        System.out.println(m.toString());
    }
 
    System.out.println("================Implicit=================");
 
}

 

위소스코드를 보면 "컬렉션은 더이상 참조가 불가능하다면서?"라는 생각이 들것이다. 하지만 t.members.size에서 size는 참조가 아니고 하나의 함수라고 생각하면된다. 

 

그렇다면 위에서 컬렉션을 더 참조하고 싶다면 

 

SELECT m.username FROM TeamJPQL t join t.members m 

 

처럼 명시적인 조인을 해주어야 연관 컬렉션의 그래프 탐색이 가능하다.

 

 

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

1. 항상 내부 조인이다.

2. 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적인 조인구문이 들어가야한다.

3. 경로 탐색은 주로 select, where 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM절에 영향을 준다.

4. 조인은 성능상 차지하는 이슈가 크다. 그리고 묵시적 조인은 예상하기 힘든 조인이기에 왠만하면 조인은

명시적조인으로 표현해주는 것이 좋다.



출처: https://coding-start.tistory.com/87?category=781616 [코딩스타트]