본문 바로가기
카테고리 없음

Class Note_QueryDSL/JPA/JPQL

by 소라소라잉 2019. 11. 28.

https://engkimbs.tistory.com/828

 

[Spring JPA #16] 스프링 데이터 QueryDsl

| QueryDsl이란 QueryDsl은 Type-Safe한 쿼리를 위한 스프링에서 제공하는 Domain Specific Language입니다. SQL같이 문자로 Type Check가 불가능하고 실행하기 전까지 작동 여부를 확인 하기 어려운 부분을 보완..

engkimbs.tistory.com

 

 

쿼리dsl이 사용하는건 Q domain 클래스라고 부른다. 

entity클래스가 바뀌면 자동으로 코드 수정

 

 

 

 

1. pom.xml 수정 

   

        <dependency>

            <groupId>org.mariadb.jdbc</groupId>

            <artifactId>mariadb-java-client</artifactId>

            <version>2.5.2</version>

        </dependency>

 

        <!-- QueryDsl 관련 라이브러리 2개-->

        <dependency>

            <groupId>com.querydsl</groupId>

            <artifactId>querydsl-jpa</artifactId>

        </dependency>

 

        <dependency>

            <groupId>com.querydsl</groupId>

            <artifactId>querydsl-apt</artifactId>

        </dependency>

 

2. BoardClass생성 

 

3. pom.xml plugin 추가 

 <plugin>

                <groupId>com.mysema.maven</groupId>

                <artifactId>apt-maven-plugin</artifactId>

                <version>1.1.3</version>

                <executions>

                    <execution>

                        <phase>generate-sources</phase>

                        <goals>

                            <goal>process</goal>

                        </goals>

                        <configuration>

                            <outputDirectory>target/generated-sources/java</outputDirectory>

                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>

                        </configuration>

                    </execution>

                </executions>

            </plugin>

 

target - generated-test-sources폴더내에 Qboard 생김! 

 

4. day3 - repositories폴더 - BoardRepository interface 생성

extends JpaRepository<Board,Integer>QuerydslPredicateExecutor

 

 

5. SearchPredicate

@Slf4j

public class BoardSearchPredicate {

 

    public static Predicate searchSimple(SearchDTO dto) {

 

        QBoard board = QBoard.board;

 

        BooleanBuilder booleanBuilder = new BooleanBuilder();

 

        if (dto.getType() == null) {

            return booleanBuilder;

        }

 

        String type = dto.getType();

        String keyword = dto.getKeyword();

 

        if (type.equals("t")) {

            booleanBuilder.and(board.title.contains(keyword));

        } else if (type.equals("c")) {

            booleanBuilder.and(board.content.contains(keyword));

        }

 

        return booleanBuilder;

 

    }

 

}

 

 

6. TEST

 

  // 제목으로 검색하는 test 

    @Test

    public void testSearchTitle() {

 

        SearchDTO dto = new SearchDTO();

        dto.setType("t");

        dto.setKeyword("5");

 

        Pageable page = PageRequest.of(0,10);

        

        // boardRepository에 predicate을 추가해서 findall하면 predicate형이 보임  

        // count Query도 where 조건이 반영되어야 한다. 실행후 where 조건이 잘 들어갔나 쿼리 확인. 

        // 분기하는 부분이 predicate뒤로 밀려남. 

        Page<Boardresult = boardRepository.findAll(BoardSearchPredicate.searchSimple(dto),page);

    }



    // 내용으로 검색하는 TEST

    @Test

    public void testSearchContent() {

        SearchDTO dto = new SearchDTO();

        dto.setType("c");

        dto.setKeyword("2");

 

        Pageable page = PageRequest.of(0,10);

        Page<Boardresult = boardRepository.findAll(BoardSearchPredicate.searchSimple(dto),page);

 

        log.info(""+ result);

    }

 

 

------- 연관관계 --------

 

 

1. 클래스고려

2. ERD고려(개체-관계 다이어그램)

3. 테이블고려 

4. 참조관계(단,양방햔) 고려 

 

 

findById( )와 getOne( ) 

getOne은 Lazy함 -> 내가 필요할때까지 안가져옴 

 

 

 

 

 

> LazyLoading과 EagerLoading에 대해 좀더 볼것. < 

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
 @Test
    public void testReadWays() {
 
        log.info("0------------------------------");
 
        // Board board = boardRepository.findById(100).get();
        // findByID 결과 
       
        /*
2019-11-28 11:16:04.353  INFO 21780 --- [           main] org.zerock.day3.BoardRepoTests           : 0------------------------------
Hibernate: 
    select
        board0_.bno as bno1_0_0_,
        board0_.content as content2_0_0_,
        board0_.regdate as regdate3_0_0_,
        board0_.title as title4_0_0_,
        board0_.update_date as update_d5_0_0_ 
    from
        d3_board board0_ 
    where
        */
 
        Board board = boardRepository.getOne(100);
        // getOne결과
        /*
        2019-11-28 11:20:27.600  INFO 19888 --- [           main] org.zerock.day3.BoardRepoTests           : 0------------------------------
2019-11-28 11:20:27.659  INFO 19888 --- [           main] org.zerock.day3.BoardRepoTests           : 1-----------------------------
2019-11-28 11:20:27.659  INFO 19888 --- [           main] org.zerock.day3.BoardRepoTests           : 100
        */
 
 
        log.info("1-----------------------------");
 
        log.info("" + board.getBno());
        log.info("" + board.getTitle());
 
        // findById : 실제로 DB를 뒤져서 싱크를 맞추고 ID에 맞는 Board객체를 만든다.  
 
        // getOne : ID값만 맞는 껍데기를 만들때 씀(쿼리가 날라가지 않음->DB를 거치지 않음.
        // new Board가 아니라 repository를 거쳐서 왔음. ->Lazy머시깽이exception) 
 
 
    }
cs

 

 

ex1 package 연습하기 

 

1 : n 

n : 1

n : n 

 

1) 각각의 독립된 클래스 정의 (Team, Palyer)

2) ERD 고려 

: 한 팀은 여러명의 선수로 구성된다. 

 선수 - collection -> Set(중복된 데이터가 없다. JPA스펙은 set이 베이스가됨. list가 순서가있다는 장점떄문에 쓰고있었지만...베이스는 set. )

- 이때 Team에 List<Player> players 선언하면 테이블 안만들어짐. 왜 ? 

각 entity가 관계가 있는데 관계설정을 안해줬기 때문! -> 어노테이션으로 관계를 설정해주자. 

proccess and point 

1) 기존에 클래스에 아무 연관관계가없었던 독립적인 두 개체는 테이블이 각각 따로 만들어짐(2개)

2) team안에 players를 추가하면 테이블이 안만들어짐(각 개체(@entity)가 관계가 있는데 관계설정을 해주지 않았기 때문)

3) 어노테이션으로 (@OneToMany) 관계설정을 해주면 3개의 테이블이 만들어짐(team,player,그리고 그둘의 관계)

 

=> 선수의 입장에서 보자면 자신은 오로지 1개의 팀을 가지기 때문에 1:1이라고도 볼 수 있는데? = 굳이 관계테이블 한개를 설정해줄 필요가 없는데? -> 따로 관계 테이블 말고 플레이어테이블에 팀컬럼을 추가해줄순 없을까? 

> JoinColumn 어노테이션! -> team_no의 이름으로 player테이블에 컬럼이 추가된다. 

 

4) 참조(단방향/양방향) 고려

: 지금까지는 단방향 참조. team만 player를 물고있고 player는 team을 참조하지 않고 있음.

 

결론 : 양방향이 좋다.

근데 왜 단뱡향을 선호하는가? => repository를 만들어서(team repository, player repository...) save할때 편함 

: player자체가 team을 물고있지 않기 때문에 객체를 만들고 save할때 편함. -> player객체만 만들고 team객체를 만들지 않아도 되니까! 

but 문제점이 있다. 실습으로 확인해보자 

 

(1) TeamRepository와 PlayerRepository Class를 만든다. 

public interface TeamRepository extends JpaRepository<Team,Integer

public interface PlayerRepository extends JpaRepository<Player,Integer

 

(2) Test cod를 만든다.

  (2-1) team과 palyer를 각각 insert한다. 

  team에 player를 넣어줘야 한다. -> 두명의 player를 가져오고 team1에 넣어주자. 

  (2-2) 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  @Test
    public void addPlayer() {
        Player p1 = playerRepo.getOne(1);
        Player p2 = playerRepo.getOne(2);
 
        Team t1 = teamRepo.getOne(1);
 
        List<Player> list = new ArrayList<>();
        list.add(p1);
        list.add(p2);
 
        t1.setPlayers(list);
 
        teamRepo.save(t1);        
    }
cs

=> Error! 

이 코드대로라면 player테이블의 p1과 p2의 team이 수정되어야 하지만 에러가 뜬다.

일단 될거면 다 되고 안될거면 다 안되야 하니까 transaction의 문제가 있음 -> 테스트메서드에 어노테이션 걸어주자(springframwork의 @Transactional) 

-> test코드는 통과하지만 DB는 수정되지 않는다. -> RollBack이 수행됨(기본적으로 test코드는 무조건 롤백) -> RollBack이 안되도록 어노테이션을 걸어주자(@commit) 

-> test코드 통과, db에도 들어감. 근데 쿼리가 좀 이상쓰(update가 세번됨)

 

Hibernate: 
    select
        team0_.tno as tno1_2_0_,
        team0_.tname as tname2_2_0_ 
    from
        d3_team team0_ 
    where
        team0_.tno=?
Hibernate: 
    select
        player0_.pno as pno1_1_0_,
        player0_.pname as pname2_1_0_ 
    from
        d3_player player0_ 
    where
        player0_.pno=?
Hibernate: 
    select
        player0_.pno as pno1_1_0_,
        player0_.pname as pname2_1_0_ 
    from
        d3_player player0_ 
    where
        player0_.pno=?

Hibernate: 
    update
        d3_player 
    set
        team_no=null 
    where
        team_no=?
Hibernate: 
    update
        d3_player 
    set
        team_no=? 
    where
        pno=?
Hibernate: 
    update
        d3_player 
    set
        team_no=? 
    where
        pno=?

 

E.M 

1. Team에서 t1을 가져옴

2. p1와 p2을 가져옴

(1.과2.는 각각)

 

 

< 단방향 >

나는 Team테이블만 뒤졌고 Player테이블은 안뒤졌음(Lazy. Player들은 어떻게 있는지 모름) 

(내가 Team을 로딩할때 Player들까지 같이 로딩하면 어떨까? -> eager )

새로운 p1과 p2를 arraylist를 집어넣음(E.M입장에서는 갱신의 의미->팀정보가 바뀌었네?(=새로운 arraylist네?) -> DB랑 동기화를 시켜줘야함.(어떻게 동기화를 시켜줄지에 대한 생각을 해야함. DB랑 어떻게 싱크를 맞출것이냐? 결론은 어쩄든 NULL로 초기화시키고 새로운 ARRAYLIST를 반영한다. )

player객체들은 team의 정보를 모름. 그래서 player의 정보를 수정하려면 team을 이용할 수 밖에 없음.

객체 입장에서 어떤 player가 자기team인지 모름.(player들은 team정보가 없기 때문에) so,  기존에 team1이었던 애들은 모두 NULL로 초기화 )

 

// 여기서 잠깐. toString의 위험한점? 

 

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
    @Test
    public void testReadTeam(){
        // 이 코드의 목적 : Lazy가 뭔지 이해하자 . 
 
        Team t1 = teamRepo.findById(1).get(); 
        // 위의 addPlayer테스트코드 때문에 t1에 player2명이 있는 상태
 
        log.info("0------------------");
        log.info(""+t1.getTno());
        log.info(""+t1.getTname());
       
        log.info(""+t1.getPlayers()); 
        // 위의 코드는 에러남. 
        // because 난 team만 뒤졌는데(LazyLoding이기때문에) 왜갑자기 player? => error 
        // 이 문제는 toString때문에 생긴다는걸 알아야함. (toString이 palyer를 호출하니까?)
 
        // 이 문제를 어떻게 해결하는가?
        // 방법1) eagerLoading하는 방법(내부적으로 Join처리 하는) 
        // 방법2) Lazy하지만 transaction 걸어서 
        // 다시 select날릴 수 있게 하는 방법(쿼리가 여러번 날라갈 수 있게 하는 방법)
        // (여러번 쿼리가 날라가려면 transaction처리가 되어야함)
 
        // 방법2를 해보자 -> @transactional 어노테이션 걸어준다. test성공
 
        // 방법1을 해보자 -> @transactional 빼고 Team.java의 어노테이션을 수정해준다.
        // @OneToMany어노테이션을 아래와 같이 수정
        // @OneToMany(fetch = FetchType.EAGER)
 
        // 방법1과 방법2의 출력결과를 확인해 보면
        // 방법2의 debug에서는 0-------과 getTno와 getTname이후에 쿼리를 하나 더 던지고
        // 플레이어들을 출력한다.
        // 방법1의 debug는 0-----,getTno,getTname,플레이어가 연달아 출력된다.
        // (애초에EAGER로딩을 했기 때문)
 
        // 그럼 데이터를 모두 EAGER로 가져오면 되잖아? -> Nope 아래 test코드를 보자 
        
    }
 
    @Test
    @Transactional
    public void testTeamPaging() {
 
 
        Pageable page = PageRequest.of(0,5, Sort.Direction.DESC, "tno");
        Page<Team> result = teamRepo.findAll(page);
        // page 처리는 count쿼리가 날라가는걸 기억하자. 
 
 
        log.info("===================================");
 
        result.forEach(t -> 
            log.info(""+t));
        // count쿼리를 제외하고 6개의 쿼리가 뜸 -> why? 출력할 team이5개라!
        // ?????????????? 
 
        // eager는 단독으로 조회를 했을때(like findById) 이거에대한 entity를 loading할때
        // eager를 쓴다. 
        // 그러나 목록(palyers)은 eager를 하는 대상이 아닌데
        // 내가 toString을 쓰면서 player목록을 봐야함.ㅠㅠ > log.info를 찍을때 진짜 eager가
        // 시작된다. 
        // team 클래스에 @toString어노테이션을 @ToString(exclude = "players")으로 하면
        // players를 제외하고 toString이 됨. 
        // 그렇게 test코드 실행해도 쿼리 6개가 날라감.
        // why? -> 내가 eager로 설정해줬으니까 palyer가 필요도 없는데 일단 eager로 실행됨.
        // Lazy로 변경해주면 해결됨.(palyers는 exclude때문에 찍히지 않지만)
        // 여기서 test코드에 @transactional 걸어주면 
 
        log.info("===================================");
    }
cs

testTeamPaging()의 목적 : DB 검색수를 최소화해야하는데 무조건 eagerLoading을 하게되면 DB접근횟수가 불필요하게 많아짐. 

 

 

 

 

<about 양방향 참조 - 11/29>

 

 

* 참고 

각각이 단독으로 crud가 가능하다 -> association

원래는 하나인데 여러개로 쪼개는애들(ex. (회원정보에서 주소를 단독으로 조회할일은 없음)회원을 통해서만 주소를 보는 것)  -> embedded -> 종속적인 관계기때문에 키를 따로 만들지 않음. 

 

 

-------------------------------- 

 

Board -> replyList 

Reply -> Board

이런 양방향 참조에서는 Board를 toString호출할시 replyList를 호출 -> reply로 들어가면 Board를 호출 -> 무한재귀호출 -> stackoverflow error -> 따라서 둘중 하나는 toStirng(exclude) 설정 해줘야함. 

 

* JPA로 할때 OOP만 고려하면 안되는 이유

ERD도 고려해야함!! -> 1:1이라고 생각할 수도 있음(댓글하나에 원글하나니까).  erd를 고려했을때 댓글과 원글과의 관계는 n:1(many to one) 임!

 

 

1. reply class만든다.

2. board에 replyList 만들고(@onetomany), reply에도 board iv선언(@manytoone).

3. 아래와 같은 테이블이 생성됨.

그러나 위의 모습은 manyToMany 일때나 쓰고싶고 onetomany에는 그냥 column추가하고 싶은데?

=> board의 replyList어노테이션수정 => @OneToMany(mappedBy = "board")

이렇게 설정하면 DB확인시 reply테이블에 board_bno컬럼이 추가됨!

 

4. 어소시에이션 걸리는 애들은 항상 toStirng exclude를 걸어주자!

=> 양방향 참조니 어소시에이션도 양방향. board, reply둘다 걸어줘야함.

 @ToString(exclude = "replyList")

@ToString(exclude = "board")

 

5. replyRepository interface 만들기

public interface ReplyRepository extends JpaRepository<Reply,Integer>{

 

* 원글/댓글과의 관계는 원글이 먼저 등록되고 나중에 댓글이 등록됨(비동시.독립적임) -> 이런경우 각각의 repository가 있어야함. (<-> 원글/첨부파일은 같이 등록됨)

 

6. 테스트해보자 

 

 

 

------

목적 : Board를 가지고 오면 reply리스트까지 역순으로 뿌려주자 (댓글 숫자도 필요하고..)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    @Test
    @Transactional
    public void testList1() {
        // bno의 역순으로 페이징 처리 
        Pageable page = PageRequest.of(0,10,Sort.Direction.DESC,"bno");
        
        Page<Board> result = boardRepository.findAll(page);
 
        result.forEach(b -> {
            log.info("BNO : "+b.getBno());
            log.info("TITLE : "+b.getTitle());
            log.info(""+b.getReplyList());
            log.info("=====================================");
        });
 
    }
 
}
cs

위 코드 => 댓글 하나 불러올때마다(log찍을때마다) reply가져오는 쿼리가 날라감(쿼리날라가고 log찍고 쿼리날라가고 log찍고..) why ? - >LazyLoading

그럼 EagerLoading하면 달라지나? => Nope!  애초에 reply가져오는 쿼리를 다 실행하고 log찍음 

 

그럼 어떤 방법으로 가져와야 하나? 

 

방법1) Fetch Join( but 다중컬렉션에 사용 불가-아래 내용 봐라-) 

관련 blog 

https://yellowh.tistory.com/133

 

[JPA]객체지향쿼리 - JPQL 페치 조인이란?

<참고자료> 자바 ORM 표준 JPA 프로그래밍 김영한 저, 에이콘 <소스코드 디자인> http://colorscripter.com 페치 조인은 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 연관된 엔티티나 컬렉션을 한번에 같이..

yellowh.tistory.com

what is Fetch Join? -> " 그냥 eager loading 시켜버려! " 

-> 이 select문을 날릴때 반드시 join으로 처리해 

 

1. BoardRepository Interface수정

1
2
3
4
5
6
7
8
public interface BoardRepository extends JpaRepository<Board,Integer>, QuerydslPredicateExecutor {
 
    // 아래 Query의 Board는 클래스명이다!-> 대소문자 틀리면 안됨
    // :bno는.... reply가 갖고있는 bno를 의미하는듯 
    @Query("SELECT b FROM Board b JOIN FETCH b.replyList WHERE b.bno = :bno ")
    public Board getBoard(Integer bno);
}
 
cs

 

2. test 

: 트랜잭션 안걸었는데 replyList까지 다 가져옴 -> join fetch의 위력! ( 첨부터 eager ! )

쿼리보면 inner join을 함 

---- 생성된 쿼리 -----

    select
        board0_.bno as bno1_0_0_,
        replylist1_.rno as rno1_2_1_,
        board0_.content as content2_0_0_,
        board0_.regdate as regdate3_0_0_,
        board0_.title as title4_0_0_,
        board0_.update_date as update_d5_0_0_,
        replylist1_.board_bno as board_bn4_2_1_,
        replylist1_.reply as reply2_2_1_,
        replylist1_.reply_date as reply_da3_2_1_,
        replylist1_.board_bno as board_bn4_2_0__,
        replylist1_.rno as rno1_2_0__ 
from
        d3_board board0_ 
    left outer join
        d3_reply replylist1_ 
            on board0_.bno=replylist1_.board_bno 
    where
        board0_.bno=?

 

그러나 댓글이 없는애들은 left outer join 해야하는데? 

repository 쿼리 수정

 @Query("SELECT b FROM Board b LEFT JOIN FETCH b.replyList WHERE b.bno = :bno ")

 

 

전체 boardList를 가져와보자

 

 

------여기넣기 ---------

 

 

 

=> 그러나 Join Fetch는

단일 collection에는 쓸 수 있으나 다중 collection에는 사용할 수 없다!!!(ex. 하나의 게시글에 첨부파일(collection), 댓글(collection)도 들어가는 것) 

 

 

 

근데 일단 게시판에서는 댓글 내용은 필요 없고 숫자만 필요한디?  

count 해보자... (여러가지방법으로) 

 

1
2
3
4
5
6
7
8
9
10
11
    // 맨 마지막 b -> 자동으로 id값으로 설정됨(bno를 의미)
    @Query("SELECT b FROM Board b LEFT JOIN FETCH b.replyList GROUP BY b")
    public List<Board> getBoardList(Pageable page);
 
    // if bno와 title이 필요하다면 그냥 select b.bno 이런식으로 쓰면 됨. 
    @Query("SELECT b, count(r) FROM Board b LEFT JOIN b.replyList r GROUP BY b")
    public List<Object[]> getBoardList2(Pageable page);
 
    @Query(value ="SELECT b, count(r) FROM Board b LEFT JOIN b.replyList r GROUP BY b",
           countQuery = "select count(b) FROM Board b")
    public Page<Object[]> getBoardList3(Pageable page);
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
    @Test
    public void testJoin() {
        Pageable page = PageRequest.of(0,10,Sort.Direction.DESC,"bno");
 
        List<Object[]> list = boardRepository.getBoardList2(page);
        
        list.forEach(arr -> {
            log.info("" + arr[0]);
            log.info(""+ arr[1]);            
        });
    }
 
    @Test
    public void testJoin2() {
        Pageable page = PageRequest.of(0,10,Sort.Direction.DESC,"bno");
 
        Page<Object[]> result = boardRepository.getBoardList3(page);
 
        log.info("" + result.getTotalElements());
        log.info("" + result.getTotalPages());
        log.info("==============================");
        
        result.forEach(arr -> {
            log.info("" + arr[0]);
            log.info(""+ arr[1]);            
        });
    }
cs

 

* fetch가 들어가면 eager라고 생각하고 join이 들어가면 그냥 우리가 아는 join이라고 생각하자

 

 

 

- controller로 json test

 

내용 

 

--------------------------

 

- swegger 이용하여 post방식으로 등록하는법 

 

 

 

 

s

 

 

 

 

 

 

 

 

 

Entity class는 기본적으로 메서드를 가지지 않는다. 

 

 

 

댓글