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<Board> result = 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<Board> result = 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는 기본적으로 메서드를 가지지 않는다.
댓글