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. 9. 13:44

JPA - JPQL(객체지향쿼리)


jpa-study.zip

(예제소스파일/ jpql package참조)

JPQL은 가장 중요한 객체지향 쿼리 언어이다. Criteria나 QueryDSL은 결국 JPQL을 편리하게 사용하도록 도와주는 기술이므로 JPA로 데이터베이스 엑세스를 다룬다면 JPQL은 꼭 필수라고 생각이 든다. SQL과 꼭 닮은 쿼리 언어이며 SQL은 데이터 중심의 쿼리라고 하면 JPQL은 엔티티를 대상으로 하는 쿼리 언어라고 할 수 있다. 결국 JPA에서 해당 JPQL을 분석한 다음 적절한 SQL로 변환해주어서 데이터베이스에서 데이터를 가져오는 것이다.

JPQL 특징

1. 엔티티 객체를 조회하는 객체지향 쿼리이다.(테이블 대상이 아니다.)
2. JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다.(데이터베이스 방언(Dialect)만 변경하면 소스 수정없이 데이터베이스 변경가능)
3. SQL보다 간결하다.






SELECT문


 


간단한 Select문이다. 일단 from절은 절대 테이블명이 아닌 엔티티명을 넣어주어야한다.(@Entity에서 name을 정의하지 않았다면 테이블명) 그리고 where절 혹은 프로젝션(select 다음 조회할 목록을 지칭) 쪽에서 엔티티의 필드를 참조하기 위해서는 반드시 클래스 내부에 정의된 필드 네임을 그대로 가야한다. 또한 from절 다음 엔티티의 별칭을 꼭 필수로 줘야한다. jqpl을 작성한 이후 쿼리를 생성할때는 2개의 객체를 반환 받을 수 있다. TypeQuery와 Query 클래스이다. 두개의 차이점은 주석에 명시되어있다. 



TypeQuery, Query


위에서 설명한 것과 같이 반환타입이 명확한가? 명확하지 않은가? 에 따라 두개중 하나의 쿼리 객체를 이용한다.




파라미터 바인딩(parameter bind)


1. 이름 기준 파라미터 - 이름 기준으로 파라미터를 구분하는 방법 ":"를 사용한다.

2. 위치 기준 파라미터 - 위치 기준으로 파라미터를 구분하는 방법 "?"를 사용한다.



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
/*
 * SELECT문
 * 1. 엔티티와 속성은 대소문자를 구분한다. but JPQL keyword는 대소구분을 않는다.
 * 2. Member는 테이블명이 아닌, 클래스명도 아닌 엔티티 명이다.
 * 3. 엔티티명에 별칭은 필수값으로 사용해야한다.
 */
public void select(){
        String jpql="select m from MemberJPQL m where m.username = '여성게'";
        /*
         * TypeQuery - 반환타입이 명확할때
         * Query - 반환타입이 명확하지 않을때
         */
        TypedQuery<MemberJPQL> query=em.createQuery(jpql,MemberJPQL.class);
 
        /*
         * getResultList() - 결과를 리스트로 받는다.(결과가 없으면 빈 컬렉션을 반환.)
         * 
         * getSingleResult() - 결과가 정확히 하나임을 기대할때 사용한다.
         *                      - 결과가 없으면 javax.persistence.NoResultException
         *                      - 결과가 하나 이상이면
                javax.persistence.NonUniqueResultException
         */
        List<MemberJPQL> resultList=query.getResultList();
 
        System.out.println("================select=================");
 
        for(MemberJPQL m:resultList){
            System.out.println(m.toString());
        }
 
        String jpql2="select m.username,m.age from MemberJPQL m where m.username = '여성게'";
        Query query2=em.createQuery(jpql2);
        List resultList2=query2.getResultList();
 
        for(Object o:resultList2){
            Object[]result=(Object[])o;
            for(Object o2:result){
                System.out.println("result element => "+o2);
            }
        }
        System.out.println("================select=================");
}
cs



위치기준 파라미터 바인딩보다 이름기준으로 바인딩하는 것이 더 명확함으로 이름기준 파라미터 바인딩 사용을 하는 것이 나을 듯 싶다.

프로젝션(projection)


SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 한다. [SELECT {프로젝션 대상} FROM~] 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다. 스칼라 타입은 숫자,문자 등 기본 데이터 타입을 뜻한다(엔티티 클래스에서 int,Long,String 등의 필드)

여기서 하나 얘기할 것이 있다면 임베디드 타입 프로젝션은 절대 조회의 시작점이 될 수 없다. 즉, From절에서 엔티티를 정의하고 select에서 엔티티.임베디드필드로 참조해야한다.(SELECT m.embeddedTypeField FROM MemberEntity m ~)



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
47
48
49
50
51
/*
 * projection - SELECT 절에 조회할 대상을 지정하는 것.
 *
 * ex) SELECT {projection} FROM ~
 *
 * 1.엔티티
 * 2.임베디드
 * 3.스칼라(숫자,문자 등 기본 데이터타입)
 */
public void projection() {
        /*
         * 엔티티타입 프로젝션 - 조회한 엔티티는 영속성컨텍스트에서 관리된다.
         */
        String jpql = "select m.team from MemberJPQL m where m.username = '여성게1'";
        TypedQuery<TeamJPQL> query = em.createQuery(jpql,TeamJPQL.class);
        List<TeamJPQL> resultList = query.getResultList();
 
        System.out.println("================projection=================");
 
        for(TeamJPQL t : resultList) {
            System.out.println(t.toString());
        }
 
        /*
         * 임베디드타입 프로젝션 - 임베디드 타입은 조회의 시작점이 될 수 없기에 Order엔티티로 조회를 시작해
         *                      그다음 임베디드 타입을 참조한다.
         */
        String jpql2 = "select o.address from OrderJPQL o where o.product.name ='맥북1'";
        TypedQuery<AddressJPQL> query2 = em.createQuery(jpql2,AddressJPQL.class);
        List<AddressJPQL> resultList2 = query2.getResultList();
 
        for(AddressJPQL o : resultList2) {
            System.out.println(o.toString());
        }
 
        /*
         * 스칼라타입 - 여러종류의 스칼라타입을 받기 위해서는 TypeQuery를 사용할 수 없다.
         * 해결책 - new 명령어를 사용한 DTO 타입변환
         * 반드시 밑 문자열에 들어간 파라미터의 순서대로 해당 dto에 생성자가 존재해야한다.
         */
        String jpql3 = "select new com.spring.jpa.jpql.MemberDTO(m.username,m.age)"
        + " from MemberJPQL m where m.username = '여성게1'";
        TypedQuery<MemberDTO> query3 = em.createQuery(jpql3,MemberDTO.class);
        List<MemberDTO> members = query3.getResultList();
 
        for(MemberDTO dto : members) {
            System.out.println(dto.toString());
        }
 
        System.out.println("================projection=================");
}
cs



위에서 조금 특이한 것이 있다면 jpql3이다. 조회할 대상이 여러개이면 TypeQuery로는 조회할 수 없기 때문에 하나의 DTO를 만들어서 위와 같이 "new" 명령어를 사용해서 결과를 DTO로 받아 TypeQuery 객체를 이용할 수 있다.



페이징 API


페이징을 하기 위해서는 다소 지루하고 반복적인 일이다. 게다가 데이터베이스 구현체에 따라 문법도 다르다. 하지만 JPA는 모든 데이터베이스에서 동일하게 사용할 수 있는 페이징 메소드를 제공해준다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void paging() {
        String jpql = "select m from MemberJPQL m ";
        TypedQuery<MemberJPQL> query = em.createQuery(jpql,MemberJPQL.class);
        //11부터 조회
        query.setFirstResult(10);
        //20개 11~30까지 조회
        query.setMaxResults(20);
        List<MemberJPQL> members = query.getResultList();
 
        System.out.println("================paging=================");
 
        for(MemberJPQL m : members) {
            System.out.println(m.toString());
        }
 
        System.out.println("================paging=================");
}
 
cs



현 예제는 값을 직접 입력해주었지만, 추후에는 컨트롤러 단에서 페이징에 대한 데이터를 직접 주입받아서 사용하지 않을까 싶다.(Pagable 클래스)






집합과 정렬



함수 

설명 

COUNT 

결과 수를 구한다. 반환타입:Long 

MAX,MIN 

최대,최소 값을 구한다. 

 AVG

평균값을 구한다. 반환타입: Double 

 SUM

합을 구한다.

정수합 반환타입 : Long

소수합 반환타입 : Double

BigInteger,BigDecimal합 반환타입 : BigInteger,BigDecimal  



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
public void groupFunction() {
        String jpql = "select count(m) from MemberJPQL m ";
        Query query = em.createQuery(jpql);
        Long count = (Long) query.getSingleResult();
 
        System.out.println("================groupFunction=================");
        System.out.println("count = "+count);
        System.out.println("================groupFunction=================");
}
 
public void groupByHaving() {
        String jpql = "select t.name, count(m.age) "
                    + "from MemberJPQL m left join m.team t "
                    + "group by t.name ";
 
        Query query = em.createQuery(jpql);
        List members = query.getResultList();
 
        System.out.println("================groupByHaving=================");
 
        for(Object o : members) {
            Object[] result = (Object[]) o;
            for(Object o2 : result) {
                System.out.print("result element => "+o2);
            }
            System.out.println();
        }
        System.out.println("================groupByHaving=================");
}
 
cs



집합함수 사용시 참고사항


1.Null 값은 무시하므로 통계에 잡히지 않는다.

2.만약 값이 없는데 집합함수를 사용한다면 Null값이 된다.(단 count는 0)

3.DISTINCT를 집합함수 안에 사용하여 중복된 값을 제거하고 집합을 구할 수 있다.

  (ex select count(distinct m.age) from MemberJPQL m)

4.DISTINCT를 count에서 사용할 때 임베디드타입 필드는 지원하지 않는다.




여기까지 JPQL포스팅을 한번 끊고 양이 많아 JPQL 조인부터는 다음 포스팅에 정리하겠습니다. :)







posted by 여성게
:
Web/JPA 2019. 2. 4. 19:02

JPA - @Embedded,@Embeddable 임베디드타입



지금까지는 엔티티에 연관관계를 제외하고는 모두 자바의 기본타입에 해당하는 값만 매핑하였다. 하지만 예를 들어서 주소라는 값을 하나의 엔티티에 매핑하고 싶은데, 도시명,구,동 이렇게 세가지의 기본타입(String)의 값을 매핑해야한다면 과연 3개를 쭉 나열하는 것이 객체지향적인 것인지 3개를 하나의 객체로 묶어서 하나의 객체로 값을 매핑하는 것이 객체지향적인 것인지 고민을 하자만 바로 후자일 것이다. 주소라는 하나의 객체를 만들고 그 안에 도시명,구,동 필드를 넣고 회원이라는 엔티티에는 주소라는 하나의 객체를 레퍼런스함으로써 조금더 객체지향적으로 엔티티를 매핑하는 방법인 것이다. 







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
import lombok.Getter;
import lombok.Setter;
import org.javers.core.metamodel.annotation.Entity;
 
@Entity
@Table(name = "CUSTOMER")
@Getter
@Setter
public class Customer {
    @Id
    @Column(name = "CUSTOMER_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "CUSTOMER_SEQ_GENERATOR")
    @TableGenerator(
            name="CUSTOMER_SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="CUSTOMER_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=1
    )
    private Long id;
 
    @Column(name = "CUSTOME_NAME")
    private String name;
 
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "CREATED_DATE")
    private Date created = new Date();
 
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "UPDATED_DATE")
    private Date updated = new Date();
 
    @Embedded
    private Address address;
 
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name="city", column = @Column(name = "COMPANY_CITY"))
            ,@AttributeOverride(name="gu", column = @Column(name = "COMPANY_GU"))
            ,@AttributeOverride(name="dong", column = @Column(name = "COMPANY_DONG"))
    })
    private Address companyAddress;
 
    @Embedded
    private PhoneNumber phoneNumber;
}
cs




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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import lombok.Getter;
import lombok.Setter;
import org.javers.core.metamodel.annotation.Entity;
 
@Embeddable
@Getter
public class Address {
    @Column(name = "CITY")
    private String city;
 
    @Column(name = "GU")
    private String gu;
 
    @Column(name = "DONG")
    private String dong;
 
    public Address() {
    }
 
    public Address(String city, String gu, String dong) {
        this.city = city;
        this.gu = gu;
        this.dong = dong;
    }
}
 
@Embeddable
@Getter
public class PhoneNumber {
    @Column(name = "AREACODE")
    private String areaCode;
 
    @Column(name = "LOCALNUMBER")
    private String localNumber;
 
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "PHONE_INFO_ID")
    private PhoneNumberInfo phoneNumberInfo;
 
    public PhoneNumber() {
    }
 
    public PhoneNumber(String areaCode, String localNumber, PhoneNumberInfo phoneNumberInfo) {
        super();
        this.areaCode = areaCode;
        this.localNumber = localNumber;
        this.phoneNumberInfo = phoneNumberInfo;
    }
}
 
@Entity
@Table(name = "PHONE_NUMBER_INFO")
@Getter
@Setter
public class PhoneNumberInfo {
    @Id
    @Column(name = "PHONE_ID")
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "PHONE_SEQ_GENERATOR")
    @TableGenerator(
            name = "PHONE_SEQ_GENERATOR",
            table = "MY_SEQUENCE",
            pkColumnName = "SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue = "PHONE_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize = 1
    )
    private Long id;
 
    @Column(name = "PHONE_ORNER_NAME")
    private String name;
 
    @Column(name = "ORNER_AGE")
    private int age;
 
    /*@OneToOne(mappedBy = "info")
    private PhoneNumber number;*/
}
cs





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
public class EmbeddedTest {
 
    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();
 
            Customer c = new Customer();
            c.setName("윤여성");
 
            Address a = new Address("서울특별시","강북구","미아동");
            Address ca = new Address("서울특별시","강북구","미아동");
 
            PhoneNumberInfo info = new PhoneNumberInfo();
 
            info.setName("윤여성고객님");
            info.setAge(28);
 
            PhoneNumber phoneNumber = new PhoneNumber("02","969-8156",info);
 
            c.setPhoneNumber(phoneNumber);
            c.setAddress(a);
            c.setCompanyAddress(ca);
 
            em.persist(c);
 
            tx.commit();
        }catch (Exception e) {
            // TODO: handle exception
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }
}
cs


위의 소스를 간단히 설명하자면 임베디드 타입의 값을 사용할 엔티티에는 @Embedded라는 어노테이션으로 해당 임베디드타입의 객체를 필드로 둔다. 그리고 임베디드 타입을 정의하는 클래스는 @Embeddable이라는 어노테이션으로 클래스를 정의해준다. 크게 복잡하지 않아 자세한 설명은 하지 않고 몇가지만 설명하겠다. 우선 위 소비자는 2개의 주소 인스턴스 필드를 갖는다. 하나는 집주소, 하나는 회사주소 그런데 주소클래스의 필드명이 같다면 데이터베이스에서 이것이 회사주소인지 집주소인지 알수 없기 때문에 소비자클래스에 @AttributeOverrides로 하나의 주소객체의 칼럼명을 변경하였다. 

그리고 핸드폰이라는 임베디드타입의 객체는 핸드폰정보라는 객체와 연관관계를 갖는다. 이렇게 임베디드타입은 단순히 매핑 뿐아니라 연관관계도 맺어줄수 있다는 점이다. 이렇게 임베디드타입을 사용하면 프로그래밍 관점에서 응집력도 훨씬 높아지기에 더욱 객체지향적인 프로그래밍 설계가 되는 것이다. 또한 임베디드타입은 어느 엔티티에서나 재사용이 가능하기 때문에 공통적인 매핑칼럼은 이렇게 임베디드타입으로 정의 해놓고 가져다 사용할 수 있다.





마지막 하나더 설명할 것이 있다면, 사실 엔티티의 값은 절대 공유되서는 안되는 값이다. 만약 1번 소비자의 주소 객체를 2번소비자의 주소객체로 레퍼런스를 전달해 공유한다면 2번 소비자의 주소객체에 수정이 일어난다면 1번 소비자의 주소객체 또한 변경이 일어날것이다.(UPDATE SQL) 그렇기 때문에 이러한 객체의 공유를 사전 차단하기 위해 @Embeddable클래스를 불변으로 만드는 것이다. 데이터 set은 생성자로만 해주고 setter자체를 생성하지 않는 것이다.(@Embeddable 클래스는 반드시 기본생성자가 필수다.) 즉, setter메소드는 만들지 않고 데이터 set용 생성자를 만들어주면 된다.(완전히 불변이라고 할 수 없지만 다른 2번소비자가 1번소비자의 주소객체의 주소만 변경해서 같은 인스턴스를 공유하는 것 정도를 사전차단하는 것이다.)

posted by 여성게
:
Web/JPA 2019. 2. 4. 17:49

JPA - 영속성 전이(Cascade)와 고아 객체(Orphan)


간단히 설명하면 영속성 전이란, 연관된 엔티티가 영속화되면, 그와 연관된 엔티티까지 모두 영속화시키는것 혹은 하나의 엔티티가 영속성 컨텍스트에서 제거가 된다면, 그와 관련된 엔티티마저 영속성 컨텍스트에서 제거가 되는 것 등의 작업흐름을 영속성 전이라고한다. 즉, 데이터베이스의 Cascade와 같은 의미이다. 


고아객체란 하나의 엔티티에서 연관된 엔티티와의 참조가 끊어지면 끊어진 엔티티를 자동으로 삭제해주는 기능이다.


두개를 예제소스로 설명하겠다.






영속성 전이(Cascade = CascadeType.xxx)



우선 예제소스를 설명하기 전에 CascadeType의 종류를 나열한다면,


1
2
3
4
5
6
7
8
public enum CascadeType{
    ALL, //모두적용
    PERSIST, //영속
    MERGE, //병합
    REMOVE, //삭제
    REPRESH, //리프래쉬
    DETACH //준영속상태로 전환
}
cs



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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import lombok.Getter;
import lombok.Setter;
import org.javers.core.metamodel.annotation.Entity;
 
@Entity
@Table(name = "TEAM_TB")
@Getter
@Setter
public class TeamFetchType {
    @Id
    private String id;
 
    private String name;
 
    @OneToMany(mappedBy="team",cascade = CascadeType.ALL)
    private List<MemberFetchTypeLazy> members = new ArrayList<MemberFetchTypeLazy>();
}
 
public class CascadeTest {
    //엔티티매니저 팩토리 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
    //엔티티매니저 생성
    EntityManager em = emf.createEntityManager();
    public void create() {
        //트랜잭션 획득
        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);
 
            team.getMembers().add(member);
 
            em.persist(team);
 
            tx.commit();
 
            em.clear();
        }catch (Exception e1) {
            // TODO: handle exception
            tx.rollback();
        }finally {
 
        }
    }
 
    public void remove() {
        //트랜잭션 획득
        EntityTransaction tx = em.getTransaction();
 
        try {
            tx.begin();
 
            TeamFetchType findTeam = em.find(TeamFetchType.class"team_1");
 
            em.remove(findTeam);
 
            tx.commit();
        }catch (Exception e1) {
            // TODO: handle exception
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }
 
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        CascadeTest t = new CascadeTest();
        t.create();
        t.remove();
 
        System.out.println();
    }
}
cs

위의 소스에서 CascadeType.ALL로 모든 영속성 전이 속성을 허용하였다. Team엔티티 인스턴스를 만들고 Member엔티티 인스턴스를 만든 다음에 Team엔티티에 add(member)를 했다.(반드시 영속성 속성이 정의된 엔티티에 객체를 add해야한다. 그래야 Team엔티티를 persist할때 Team필드에 add된 Member까지 persist된다.) 그리고 em.persist()호출하니 Team엔티티는 물론 Member엔티티까지 데이터베이스에 insert 됬다. 그리고 위 remove()에서 em.remove(findTeam)을 호출하는 순간 Team 엔티티가 삭제됨과 동시에 Member 엔티티도 모두 삭제되는 것을 볼 수 있다. 만약 영속성 전이 속성을 사용하지 않았다면 Team 엔티티를 삭제하는 순간 외래키 참조무결성 조약에 어긋나서 예외가 발생할 것이다. 즉, 영속성 전이 속성없이 외래키의 주인인 Member를 지우는 것은 문제가 안되지만 이미 Member가 참조하고 있는 Team을 삭제하는 순간 문제가 되는 것이다. 그냥 쉽게 생각하면 데이터베이스에 Cascade 키워드를 사용하는 것이라고 생각하면 된다.






고아객체(orphan)



위에서도 간단히 설명했지만 고아객체란 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제되는 기능이다.


소스에서도 보듯이 findTeam.getMembers().clear() 메소드를 호출했을 뿐인데, 플러쉬 단계에서 해당 Member엔티티를 삭제한다. 즉, 고아객체 속성이 적용된 하나의 엔티티에서 연관관계에 있는 엔티티의 참조를 제거하는 것만으로 연관된 엔티티가 삭제되는 것이다.


하지만 주의할 점은 고아객체 속성은 반드시 참조가 제거된 엔티티가 어디에서든 참조하지 않는 객체일때만 사용해야한다. 만약 다른데에서도 참조하는 엔티티인데 삭제가 된다면 문제가 생길 수 있다.

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 여성게
:
Web/JPA 2019. 2. 3. 00:09

JPA - 하나의 엔티티에 다수 테이블 매핑(@SecondaryTable,@SecondaryTables)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
@Table(name = "BOARD")
@SecondaryTable(name = "BOARD_DETAIL",
                pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAL_ID"))
@Getter
@Setter
public class Board {
    @Id
    @Column(name = "BOARD_ID")
    private String id;
    private String title;
    @Column(table = "BOARD_DETAIL",
            name = "BOARD_CONTENT")
    private String content;
}
cs



별거없이 위의 코드가 전부이다. 여기서 하나만 설명하자면 @PrimaryKeyJoinColumn을 사용하지 않으면 BOARD_ID를 그대로 가져간다. 왠지 상속관계에서 이 어노테이션을 사용하였는데 내부적으로 처리가 비슷하게 되는것 같다. 이 방법은 사용을 안하는 것이 좋다. 하나의 엔티티를 조회하는데 2개의 테이블을 조회해와야 하기에 최적화하기가 쉽지 않다. 만약 무조건 연관되어 두개의 테이블 모두 가져와야한다면 써도 좋지만 왠만하면 테이블당 하나씩 엔티티를 매핑하는 것이 좋다.


만약 2개 이상의 테이블을 하나의 엔티티에 매핑하려면


1
2
3
4
@SecondaryTables({
    @SecondaryTable(name = "BOARD_DETAIL),
    @SecondaryTable(name = "BOARD_FILE)
})
cs


로 매핑하면 된다! 다시한번 말하지만 최대한 이 방법을 사용하지 않는 방향으로 가는 것이 좋을 듯 싶다!!

posted by 여성게
:
Web/JPA 2019. 2. 2. 22:35

@MappedSuperClass


바로 직전의 포스트에서는 JPA에서의 엔티티 상속 전략에 대해서 다루었다. 이번에 다룰 것은 비슷한 상속개념이지만, 이전 포스트 글과는 다르게 부모에 해당하는 클래스가 테이블과 매핑되는 것이 아니라, 단순 상속받는 엔티티에게 매핑정보만 상속해주는 것이다. 즉, 객체들이 주로 사용하는 공통 매핑정보등을 정의하여 그 매핑정보를 엔티티들에게 상속해주는 것이다.





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
@MappedSuperclass
@Getter
@Setter
public abstract class BaseEntity {
    @Id
    @Column(name = "BASE_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "HUMAN_SEQ_GENERATOR")
    @TableGenerator(
            name="HUMAN_SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME", //MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="HUMAN_SEQ", //SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=1
    )
    private Long id;
 
    private String name;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date created = new Date();
}
 
@Entity
@Getter
@Setter
@AttributeOverride(name = "id", column = @Column(name = "EMPLOYEE_ID"))
public class Employee extends BaseEntity{
    private String email;
}
cs


코드만 보더라도 직관적이다. @MappedSuperClass 어노테이션이 붙은 추상클래스가 매핑정보를 상속해줄 클래스이다. 코드에서 보듯이 공통적인 매핑정보를 가지고 있다. 여기서 한가지 중요한 것은 @MappedSuperClass가 붙은 클래스는 절대 엔티티가 아니기 때문에 영속성컨텍스트에서 별도로 가지고 올 수 없다.


밑의 엔티티 클래스에서 설명할 어노테이션은 @AttributeOverride이다.  만약 추상클래스에서 정의한 컬럼명을 그대로 상속받는 것이 아니고 컬럼명을 변경하고 싶을 때는 위 어노테이션을 써서 변경하면 된다. 만약 변경할 컬럼이 여러개라면 


1
2
3
4
@AttributeOverrides({
    @AttributeOverride(name = "id", column = @Column = .....
    ,@AttributeOverride(name = "name", column = @Column = .....
})
cs


로 매핑정보들을 오버라이드 할 수 있다.




posted by 여성게
:
Web/JPA 2019. 2. 2. 22:17

JPA - 상속 관계 매핑, @Inheritance, @DiscriminatorColumn



사실 관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없다. 대신 슈퍼타입 서브타입관계라는 모델링 기법이 객체의 상속 개념과 가장 유사하다. ORM에서 이야기하는 상속 관계 매핑은 객체의 상속구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것이다.


슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현할 때는 3가지 방법을 선택할 수 있다.





  

 각각의 테이블로 변환

 4개 각각을 모두 테이블로 만들고 조회할 때 조인을 사용한다.(조인전략)

 통합 테이블로 변환

 단 하나의 테이블을 사용해서 상속관계를 통합한다.(단일 테이블 전략)

 서브타입 테이블로 변환

 서브 타입마다 하나의 테이블을 만든다(Entity-per-table 전략)







조인전략


조인전략은 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본키 + 외래키로 사용하는 전략이다. 조회할 때 조인을 자주 사용한다. 그리고 이 전략을 사용할 때 주의할 점이 객체는 타입으로 구분가능하지만 테이블은 타입의 개념이 없다. 즉, 부모테이블에서 자식테이블의 타입을 구분할 칼럼이 존재해야한다.



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
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
//자식테이블을 구분할 구분자 컬럼이름을 지어준다.
@DiscriminatorColumn(name = "DTYPE")
@Getter
@Setter
public abstract class Item {
    @Id
    @Column(name = "ITEM_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "ITEM_SEQ_GENERATOR")
    @TableGenerator(
            name="ITEM_SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="ITEM_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=1
    )
    private Long id;
 
    private String name;
 
    private int price;
}
 
@Entity
@DiscriminatorValue("Album")
/*
 * 상속매핑을 할때 디폴트로 자식테이블은 부모테이블의 ID칼럼명을 그대로 사용하는데,
 * 만약 자식테이블의 기본 키 칼럼명을 재정의 하고 싶다면 밑의 어노테이션으로 하면된다.
 */
@PrimaryKeyJoinColumn(name = "ALBUM_ID")
@Getter
@Setter
public class Album extends Item{
    private String artist;
}
 
@Entity
//default = entity class name
@DiscriminatorValue("MOVIE")
@Getter
@Setter
public class Movie extends Item{
    private String director;
    private String actor;
}
cs



위와 같이 조인전략을 이용하여 상속관계를 정의해주었다. 대부분의 어노테이션은 주석으로 충분히 설명이 될 것같아서 생략한다.


구분 

설명 

장점 

-테이블이 정규화된다.

-외래 키 참조 무결성 제약조건을 활용할 수 있다.

-저장공간을 효율적으로 사용한다 

 단점

-조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.

-조회 쿼리가 복잡하다.

-데이터를 등록할 INSERT SQL을 두번 실행한다.(부모,자식테이블)

 특징

-JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇몇 구현체는 구분컬럼(@DisciminatorColumn) 없이도 동작한다.






단일 테이블 전략



테이블을 하나만 사용하는 전략이다. 구분칼럼으로 어떤 자식 데이터가 저장되었는지 구분한다. 조회할때 조인이 필요없으므로 일반적으로 3전략중 가장 빠르다.

하지만 이 전략을 사용할 때 주의점은 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다는 점이다. 예를 들어서 Book엔티티를 저장하면 나머지 자식 엔티티의 필드는 필요없기에 null로 들어가기 때문이다.


소스는 위의 소스에서 @Inheritance(strategy = InheritanceType.SINGLE_TABLE)로 지정해주면 된다.


구분 

설명 

장점 

-조인이 필요없으므로 일반적으로 조회 속도가 빠르다

-조회 쿼리가 단순하다.

단점 

-자식 엔티티가 매핑한 칼럼은 모두 null을 허용해야한다.

-단일 테이블에 모든 것을 저장하므로 자식엔티티가 많아지거나 컬럼수가 각 엔티티별로 많다면  테이블이 엄청 커져서 오히려 조회 성능이 나빠질 수 있다.

특징 

-구분 칼럼을 꼭 사용해야한다. 따라서 @DiscriminatorColumndms 꼭 필수 어노테이션이다. 




Table-per-Concrete-Class 전략



자식 엔티티마다 테이블을 만드는 전략이다.

소스는 위와같이 strategy = InheritanceType.TABLE_PER_CLASS로 바꿔주면된다. 이 전략은 데이터베이스 설계자와 ORM전문가 둘 다 일반적으로 추천하지 않는 전략이라고 한다.


구분 

설명 

장점 

-서브타입을 구분해서 처리할 때 효과적이다.

-not null 제약조건을 사용할 수 있다. 

단점 

-여러 자식 테이블을 함께 조회할 때 성능이 느리다.(SQL의 UNION사용)

-자식 테이블을 통합해서 쿼리하기 어렵다. 

특징 

-구분 컬럼을 사용하지 않는다. 



posted by 여성게
: