Web/JPA 2019. 2. 9. 14:41

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



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



내부 조인(inner join)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
 * 내부조인
 */
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=================");
 
}
cs



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


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





외부조인(outer join)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
 * 외부조인
 */
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=================");
 
}
cs




컬렉션 조인(Collection Join)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
 * 컬렉션조인 - 일대다,다대다 관계처럼 필드에 컬렉션을 사용하는 곳에 조인하는 것.
 * 
 * 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=================");
 
}
cs



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




세타조인(Theta Join)



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



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
 * 세타조인 - 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=================");
 
}
cs





Join on 절



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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
 * 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=================");
 
}
cs




패치조인(fetch Join)



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


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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
 * 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=================");
 
}
cs



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를 사용한다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
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=================");
 
}
cs





바뀐 것은 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절에서 조인을 통해 별칭을 얻는다면 계속해서 탐색가능하다. 



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
 * 묵시적 조인이다. 
 * 명시적으로 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=================");
 
}
cs



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


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


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


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



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

1. 항상 내부 조인이다.

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

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

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

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












posted by 여성게
:
Web/JPA 2019. 2. 4. 16:42

JPA - 즉시로딩과 지연로딩(FetchType.EAGER,FetchType.LAZY) 그리고 프록시



만약 회원이라는 엔티티 객체와 팀이라는 엔티티 객체가 있고 회원:팀 = N:1 연관관계를 맺고 있다고 가정하자. 만약 회원이라는 엔티티를 데이터베이스에서 조회했을 경우 팀이라는 엔티티 객체를 같이 로딩해서 사용할 수 도 있겠지만 진짜 회원만 사용할 목적으로 엔티티객체를 조회 할 수도 있다. 그렇다면 만약 필요하지 않은 연관관계 객체의 로딩을 뒤로 미룬다면 어떻게 할까? 이것은 불필요한 데이터베이스 조회 성능을 최적화 할 수 있는 기회가 될 수 있을 것이다. 예를 들어 연관관계가 List 필드로 되어있고 연관된 객체가 수만개라면? 그리고 해당 List연관관계의 엔티티는 필요하지 않은 상황이라면? 이럴경우에는 지연로딩이라는 패치전략을 사용할 수 있다. 그리고 필요한 시점에 그 객체를 데이터베이스에서 불러올 수 있다. 






지연로딩, FetchType.LAZY 그리고 프록시 객체



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Entity
@Table(name = "MEMBER_TB")
@Getter
@Setter
public class MemberFetchTypeLazy {
    @Id
    private String id;
 
    private String name;
 
    @ManyToOne(fetch = FetchType.LAZY)
    private TeamFetchType team;
}
 
@Entity
@Table(name = "TEAM_TB")
@Getter
@Setter
public class TeamFetchType {
    @Id
    private String id;
 
    private String name;
 
    @OneToMany(mappedBy="team")
    private List<MemberFetchTypeLazy> members = new ArrayList<MemberFetchTypeLazy>();
}
cs


지연로딩 전략은 위와 같이 연관관계를 명시해주는 어노테이션에 fetch = FetchType.Lazy 처럼 명시 해줄 수 있다. 위의 소스 설명은 회원엔티티를 조회할때 팀엔티티를 즉시로딩하지 않고 지연로딩할 것이라는 소스이다. 이 말은 회원엔티티를 조회할때 팀회원을 같이 데이터베이스에서 가져오지 않고, MemberFetchTypeLazy.getTeam() 으로 실제로 팀엔티티가 사용될 때 데이터베이스에서 해당 엔티티를 조회해오는 것이다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class FetchTypeTest {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        //엔티티매니저 팩토리 생성
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
 
        //엔티티매니저 생성
        EntityManager em = emf.createEntityManager();
 
        //트랜잭션 획득
        EntityTransaction tx = em.getTransaction();
 
        try {
            tx.begin();
 
            TeamFetchType team = new TeamFetchType();
            team.setId("team_1");
            team.setName("team_1");
 
            em.persist(team);
 
            MemberFetchTypeLazy member = new MemberFetchTypeLazy();
            member.setId("member_1");
            member.setName("윤여성");
            member.setTeam(team);
 
            em.persist(member);
 
            tx.commit();
        }catch (Exception e1) {
            // TODO: handle exception
            tx.rollback();
        }finally {
            em.close();
        }
        /*emf.close();*/
 
        //엔티티매니저를 새로 생성한 이유는 영속성컨텍스트가 비어있는 상태에서 조회하기 위함(em.clear()도 가능)
        EntityManager em2 = emf.createEntityManager();
 
        MemberFetchTypeLazy findMember = em2.find(MemberFetchTypeLazy.class"member_1");
 
        System.out.println(findMember.getTeam().getName());
        System.out.println();
    }
}
cs






(디버깅화면 잘보세요 !) 

첫번째 결과 사진을 보면 회원엔티티를 조회하면 team필드에는 id=null,name=null이 들어가있는 것이 보일 것이다. 즉, 데이터베이스에서 조회해오지 않는다. 그리고 Console에도 보면 회원엔티티만을 조회하고 있다.


두번째 결과 getTeam().getName() 이후 Console에는 팀을 조회하는 SQL이 생겨나있는 것을 볼 수 있다. 이말은 회원엔티티를 조회할때는 팀엔티티를 가져오지 않고 진짜 팀엔티티가 사용될 시점에 데이터베이스에서 팀 엔티티를 조회해오는 것이다. 


여기서 디버깅화면을 보면 team필드에 이상한 인스턴스이름이 들어가 있는 것을 볼 수 있다.



바로 지연로딩의 핵심이되는 프록시라는 객체가 team필드가 레퍼런스하고 있는 것이다. 맞다. JPA는 지연로딩에 프록시전략을 이용한다. 프록시는 간략히 얘기하면 대행자이다. 팀회원을 조회할때 team에는 실제 팀엔티티가 들어가는 것이 아니고 프록시 객체가 들어가는 것이고, 실제 팀엔티티를 사용할때 이 프록시객체가 대행자역할을 하여 팀엔티티를 바라보고 대신 팀엔티티관련 데이터를 리턴해주는 것이다.(하지만 만약 Team 엔티티가 이미 영속성컨텍스트에 들어가있다면 지연로딩설정을 해도 즉시로딩과 동일하게 영속성 컨텍스트에서 Team엔티티를 가져온다. 그래서 위의 예제는 영속성컨텍스트를 초기화? 할 목적으로 엔티티매니저 인스턴스를 새로 생성하였다.)



프록시 객체를 실제 엔티티를 상속하고 있기 때문에 겉모양을 그대로이다. 그리고 프록시를 내부적으로 실제 엔티티에 대한 레퍼런스를 갖고 직접 실제객체의 데이터를 리턴해준다. 


그리고 한가지더 얘기할 것은 위와 같이 Member.getTeam을 조회할때는 프록시 객체가 엔티티를 초기화요청을 한다. 하지만 엔티티의 필드중 컬렉션으로 되어있는 필드를 초기화하려면 Team.getMembers.get(i)처럼 직접 실제 인덱스로 데이터를 조회할 경우 엔티티초기화요청을 프록시 객체가 보내게 된다. 


<엔티티 필드 중에 컬렉션은 진짜 컬렉션으로 저장될까?>


JPA는 필드중 컬렉션으로 된 타입이 있다면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경한다. 이것을 컬렉션 래퍼라고 한다. 디버깅을 해보면 컬렉션으로 된 타입의 필드가 있다면 PersistentBag라는 컬렉션래퍼로 반환되는 것을 볼 수 있다.





FetchType.EAGER,즉시로딩


 

즉시로딩은 어노테이션만 EAGER로 바꾸어주면 된다. 즉시로딩은 말그대로 즉시 연관된 엔티티도 다 조회를 해오는 것이다. 어려운 설정은 없다. 하지만 하나 주의해야될 것이 있다. 즉시로딩은 연관된 엔티티를 따로따로 조회하는 것이 아니라, 조인을 이용해 하나의 쿼리로 데이터를 가져오기 때문에 만약 외래키에게 널을 허용한다면? 외부조인을, 외래키가 널을 허용하지 않는다면 내부조인을 이용해서 가져온다. (선택적 비식별관계, 필수적 비식별관계)


이것이 주의해야될 상황인것이 만약 특정상황에서 필요한 데이터를 가져오지 못하는 상황이 발생 할 수 있다.(내부조인,외부조인) 

- 외부조인이라면 null값이 저장된 데이터는 가져오지 않는다. 즉, 필요할 수 있는 데이터를 외래키null을 허용함으로써 가져오지 못한다.


<주의사항>

-컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.

 :A 테이블을 N,M 두 테이블과 일대다 조인한다면 SQL 실행 결과가 N곱하기 M이 되면서 너무 많은 데이터를 반환할 수 있고 결과적으로 애플리케이션 성능이 저하될 수 있다.





JPA 기본 패치전략



- @ManyToOne, @OneToOne : 즉시로딩(FetchType.EAGER)

- @OneToMany,@ManyToMany :  지연로딩(FetchType.LAZY)


하지만 대게 정말 필요한 상황을 제외하고는 LAZY, 지연로딩하는 것을 권고한다고 한다.



<optional 속성>


@ManyToOne, @OneToOne (optional = false, true)

 false : 내부 조인

 true : 외부조인


- @OneToMany,@ManyToMany (optional = false, true)

 false : 외부 조인

 true : 외부조인


여기서 특징이라면 엔티티의 컬렉션을 조회할때는 optional 속성과 무관하게 무조건 외부조인을 이용한다는 점이다.


   


posted by 여성게
: