QueryDsl 에 대해 학습을 하였으므로 상황을 가정함으로써 앞으로 잘 쓸 수 있도록 준비를 해보겠다.
Member
에 대해 검색할 때 사용자의 나이, 이름, 그 사용자가 속한 team
의 quality , name 으로 검색 이 가능 하다.build.gradle
에 세팅은 모두 되어있다고 가정한다.
@Configuration
@RequiredArgsConstructor
public class QuerydslConfig {
private final EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager em){
return new JPAQueryFactory(em);
}
}
쿼리 Dsl 에 필요한 JPAQueryFactor
를 빈으로 등록하기 위해 설정해준다.
@Component
@RequiredArgsConstructor
public class initMemberAndTeam {
private final InitMemberAndTeamService initMemberService;
@PostConstruct
public void init(){
initMemberService.init();
}
@Component
static class InitMemberAndTeamService{
@PersistenceContext
EntityManager em;
@Transactional
public void init(){
Team teamA = Team.builder().quality(3).name("teamA").build();
Team teamB = Team.builder().quality(1).name("teamB").build();
Team teamC = Team.builder().quality(4).name("teamC").build();
Team teamD = Team.builder().quality(2).name("teamD").build();
Team teamE = Team.builder().quality(6).name("teamE").build();
Team teamF = Team.builder().quality(5).name("teamF").build();
em.persist(teamA);
em.persist(teamB);
em.persist(teamC);
em.persist(teamD);
em.persist(teamE);
em.persist(teamF);
for(int i=0;i<100;i++){
Team team = null;
switch (i%6){
case 0:
team=teamA;
break;
case 1:
team=teamB;
break;
case 2:
team=teamC;
break;
case 3:
team=teamD;
break;
case 4:
team=teamE;
break;
case 5:
team=teamF;
break;
}
Member member = Member.builder().name("member"+i).age(100-i).team(team).build();
em.persist(member);
}
}
}
}
실제로 정렬이나 동적 쿼리가 잘 작동되는지 확인을 위한 클래스이다.
Member 와 Team 에 대한 데이터를 넣어준다.
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member extends BaseEntity {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private int age;
@Builder
public Member(String name,int age,Team team){
this.name=name;
this.team=team;
this.age =age;
}
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
@JoinColumn
으로 다대일 세팅을 해준다.@Entity
@Getter
@Setter
@NoArgsConstructor
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int quality;
@Builder
public Team(String name, int quality){
this.name=name;
this.quality = quality;
}
@OneToMany(mappedBy = "team",cascade = CascadeType.ALL,orphanRemoval = true)
private List<Member> members=new ArrayList<>();
}
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class MemberResponseWithTeamDto {
private Long memberId;
private Long teamId;
private String memberName;
private String teamName;
private int teamQuality;
private int memberAge;
public static MemberResponseWithTeamDto of(Member member){
return new MemberResponseWithTeamDto(member.getId(),member.getTeam().getId(),member.getName(),member.getTeam().getName(),
member.getTeam().getQuality(),member.getAge());
}
}
공통적으로 프론트에게 반환하는 클래스 이다 .
@Data
public class MemberSearch {
private Integer teamQuality;
private Integer minAge;
private Integer maxAge;
private String memberName;
private String teamName;
}
사용자가 동적으로 검색할 때 활용하는 class 이다. 일단 request dto 와 비슷한 역할을 한다고 생각한다.
원시타입이 아닌 Integer
이런 식으로 준 이유는
밑에서 Null 체크를 하기 때문이다.
@Getter
@AllArgsConstructor(access = AccessLevel.PUBLIC)
public class MemberSearchResponseWithPageDto {
private long totalCount;
private List<MemberResponseWithTeamDto> memberResponseWithTeamDtoList = new ArrayList<>();
private int totalPage;
}
Page 방식을 반환하기를 원할 때 실제 내용과 총 내용의 개수, 총 page 의 개수를 반환하였기에 위와 같이 클래스를 설정해준다.
@Getter
@AllArgsConstructor(access = AccessLevel.PUBLIC)
public class MemberSearchResponseWithSliceDto {
private boolean hasNextPage;
private List<MemberResponseWithTeamDto> memberResponseWithTeamDtoList = new ArrayList<>();
}
마찬가지로 Slice 방식은 다음 페이지가 있는 지 여부와 실제 내용을 함께 반환하기에 위와 같이 클래스를 설정한다.
public interface MemberCustomRepository {
public Slice<Member> showMemberWithSlice(MemberSearch memberSearch, Pageable pageable);
public Page<Member> showMemberWithPage(MemberSearch memberSearch,Pageable pageable);
}
@RequiredArgsConstructor
public class MemberCustomRepositoryImpl implements MemberCustomRepository{
private final JPAQueryFactory queryFactory;
private static final String MEMBER_NAME="memberName";
private static final String TEAM_NAME="teamName";
private static final String AGE="age";
private static final String QUALITY = "quality";
@Override
public Page<Member> showMemberWithPage(MemberSearch memberSearch, Pageable pageable) {
List<Member> content = queryFactory
.selectFrom(member)
.join(member.team,team).fetchJoin()
.where(usernameEq(memberSearch.getMemberName()),
teamNameEq(memberSearch.getTeamName()),
teamQualityEq(memberSearch.getTeamQuality()),
ageLoe(memberSearch.getMaxAge()),
ageGoe(memberSearch.getMinAge()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(memberSort(pageable))
.fetch();
JPAQuery<Long> countQuery = queryFactory
.select(member.count())
.from(member)
// .join(member.team,team).fetchJoin()
.where(usernameEq(memberSearch.getMemberName()),
teamNameEq(memberSearch.getTeamName()),
teamQualityEq(memberSearch.getTeamQuality()),
ageLoe(memberSearch.getMaxAge()),
ageGoe(memberSearch.getMinAge()));
Long count = queryFactory
.select(member.count())
.from(member)
// .join(member.team,team).fetchJoin()
.where(usernameEq(memberSearch.getMemberName()),
teamNameEq(memberSearch.getTeamName()),
teamQualityEq(memberSearch.getTeamQuality()),
ageLoe(memberSearch.getMaxAge()),
ageGoe(memberSearch.getMinAge()))
.fetchOne();
return new PageImpl<>(content,pageable,count);
// return PageableExecutionUtils.getPage(content,pageable,countQuery::fetchOne);
}
@Override
public Slice<Member> showMemberWithSlice(MemberSearch memberSearch, Pageable pageable) {
List<Member> memberList = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(usernameEq(memberSearch.getMemberName()),
teamNameEq(memberSearch.getTeamName()),
teamQualityEq(memberSearch.getTeamQuality()),
ageLoe(memberSearch.getMaxAge()),
ageGoe(memberSearch.getMinAge()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize()+1)
.orderBy(memberSort(pageable))
.fetch();
return checkEndPage(memberList,pageable);
}
private Slice<Member> checkEndPage(List<Member> memberList,Pageable pageable){
boolean hasNext = false;
System.out.println("result 의 size"+memberList.size());
System.out.println("pageable 의 getPageSize()"+pageable.getPageSize());
if(memberList.size()> pageable.getPageSize()){
hasNext = true;
memberList.remove(pageable.getPageSize());
}
return new SliceImpl<>(memberList,pageable,hasNext);
}
private BooleanExpression usernameEq(String username){
return username ==null?null:member.name.eq(username);
}
private BooleanExpression teamNameEq(String teamName){
return teamName==null?null:team.name.eq(teamName);
}
private BooleanExpression teamQualityEq(Integer quality){
return quality == null ? null: member.team.quality.eq(quality);
}
private BooleanExpression ageLoe(Integer ageLoe){
return ageLoe == null ? null : member.age.loe(ageLoe);
}
private BooleanExpression ageGoe(Integer ageGoe){
return ageGoe == null ? null : member.age.goe(ageGoe);
}
private OrderSpecifier<?> memberSort(Pageable pageable){
if(!pageable.getSort().isEmpty()){
for(Sort.Order order: pageable.getSort()){
Order direction = order.getDirection().isAscending()? Order.ASC:Order.DESC;
switch (order.getProperty()){
case MEMBER_NAME:
return new OrderSpecifier(direction,member.name);
case TEAM_NAME:
return new OrderSpecifier(direction,member.team.name);
case AGE:
return new OrderSpecifier(direction,member.age);
case QUALITY:
return new OrderSpecifier(direction,team.quality);
}
}
}
return new OrderSpecifier(Order.ASC,member.id);
}
}
이 메서드는 Slice 방식과 Page 방식 모두 정렬 을 위해서 사용해야 하는 클래스이다.
private OrderSpecifier<?> memberSort(Pageable pageable){
if(!pageable.getSort().isEmpty()){
for(Sort.Order order: pageable.getSort()){
Order direction = order.getDirection().isAscending()? Order.ASC:Order.DESC;
switch (order.getProperty()){
case MEMBER_NAME:
return new OrderSpecifier(direction,member.name);
case TEAM_NAME:
return new OrderSpecifier(direction,member.team.name);
case AGE:
return new OrderSpecifier(direction,member.age);
case QUALITY:
return new OrderSpecifier(direction,team.quality);
}
}
}
return new OrderSpecifier(Order.ASC,member.id);
}
처음에 sort 에 대한 정보가 들어왔는지 판단한다. 만약 Sort 에 대한 정보가 들어오지 않았다면 기본적인 정렬 방식인 member의 id 에 대해 증가하는 방식으로 반환했다.
Null
을 반환하고 싶지만 그러면 에러가 나기 때문에 (위에서 사용되는 OrderBy(Null) 이 불가) 설정해 주었다.
나머지 if 문 안에 있는 코드는 정렬 방식을 받고 그 정렬 방식의 필드, direction 에 따라 OrderSpecifier
의 정렬 필드, dirction 을 설정해준다.
case 다음 문에는 클래스 상단에 static final
로 설정 해주었다.
private BooleanExpression usernameEq(String username){
return username ==null?null:member.name.eq(username);
}
private BooleanExpression teamNameEq(String teamName){
return teamName==null?null:team.name.eq(teamName);
}
private BooleanExpression teamQualityEq(Integer quality){
return quality == null ? null: member.team.quality.eq(quality);
}
private BooleanExpression ageLoe(Integer ageLoe){
return ageLoe == null ? null : member.age.loe(ageLoe);
}
private BooleanExpression ageGoe(Integer ageGoe){
return ageGoe == null ? null : member.age.goe(ageGoe);
}
BooleanExpression
을 Where
절에 넣어 주고 ,
로 연결 시킬 수 있다.
이 과정에서 BooleanExpression
의 반환값이 Null
이라면 Where
절에서 무시가 되기 때문에 동적으로 쿼리를 발생시킬 수 있는 원동력이 된다.
age=null & memberName= jinu
가 아니라
memberName = jinu
만 where 절에 넣을 수 있다는 말이다.
@Override
public Slice<Member> showMemberWithSlice(MemberSearch memberSearch, Pageable pageable) {
List<Member> memberList = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(usernameEq(memberSearch.getMemberName()),
teamNameEq(memberSearch.getTeamName()),
teamQualityEq(memberSearch.getTeamQuality()),
ageLoe(memberSearch.getMaxAge()),
ageGoe(memberSearch.getMinAge()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize()+1)
.orderBy(memberSort(pageable))
.fetch();
return checkEndPage(memberList,pageable);
}
private Slice<Member> checkEndPage(List<Member> memberList,Pageable pageable){
boolean hasNext = false;
System.out.println("result 의 size"+memberList.size());
System.out.println("pageable 의 getPageSize()"+pageable.getPageSize());
if(memberList.size()> pageable.getPageSize()){
hasNext = true;
memberList.remove(pageable.getPageSize());
}
return new SliceImpl<>(memberList,pageable,hasNext);
}
Slice 방식은 전체 count 쿼리가 필요없는 무한 스크롤 방식에 주로 사용된다.
원래 스프링 data jpa 에서 Slice 를 이용했을 떄 자동으로 hasNextPage
를 가져와서 사용하면 이게 다음 페이지가 있는지 확인이 가능했는데
쿼리 Dsl 에서는 직접 이 과정을 해줘야 한다고 한다.
.offset(pageable.getOffset())
.limit(pageable.getPageSize()+1)
.orderBy(memberSort(pageable))
.fetch();
page=X
이렇게 온다면pageable.getOffset() 의 값은 X 곱하기 pageSize
값을 반환한다.
디폴트가 size 가 20이고, page =3 이라면
60을 반환할 것이다. 아무튼 시작 지점을 반환한다.
limit(pageable.getPageSize()+1) 을 해주는 이유는 실제 요청한 것보다 limit 를 1증가시켜 다음 페이지가 있는지 확인하기 위한 작업이다.
order by 는 아까 그 만든 메서드를 넣어준다 .
private Slice<Member> checkEndPage(List<Member> memberList,Pageable pageable){
boolean hasNext = false;
System.out.println("result 의 size"+memberList.size());
System.out.println("pageable 의 getPageSize()"+pageable.getPageSize());
if(memberList.size()> pageable.getPageSize()){
hasNext = true;
memberList.remove(pageable.getPageSize());
}
return new SliceImpl<>(memberList,pageable,hasNext);
}
getPageSize()
) 보다 실제 나온 개수(memberList.getSize()
(아까 1 더해준 것) 가 같거나 작다는 의미이다. @Override
public Page<Member> showMemberWithPage(MemberSearch memberSearch, Pageable pageable) {
List<Member> content = queryFactory
.selectFrom(member)
.join(member.team,team).fetchJoin()
.where(usernameEq(memberSearch.getMemberName()),
teamNameEq(memberSearch.getTeamName()),
teamQualityEq(memberSearch.getTeamQuality()),
ageLoe(memberSearch.getMaxAge()),
ageGoe(memberSearch.getMinAge()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(memberSort(pageable))
.fetch();
JPAQuery<Long> countQuery = queryFactory
.select(member.count())
.from(member)
.where(usernameEq(memberSearch.getMemberName()),
teamNameEq(memberSearch.getTeamName()),
teamQualityEq(memberSearch.getTeamQuality()),
ageLoe(memberSearch.getMaxAge()),
ageGoe(memberSearch.getMinAge()));
Long count = queryFactory
.select(member.count())
.from(member)
.where(usernameEq(memberSearch.getMemberName()),
teamNameEq(memberSearch.getTeamName()),
teamQualityEq(memberSearch.getTeamQuality()),
ageLoe(memberSearch.getMaxAge()),
ageGoe(memberSearch.getMinAge()))
.fetchOne();
return new PageImpl<>(content,pageable,count);
// return PageableExecutionUtils.getPage(content,pageable,countQuery::fetchOne);
}
Page는 Slice 와 다르게 total 개수 에 대한 count 쿼리가 날라간다.
이를 직접 구현해준다. (
QueryResults
를 반환형으로 받으면 (끝에 .fetchResults()를 붙이면 됨)
손쉽게 results.getTotal()
로 Count 값을 구할 수 있으나
1) 불필요한 조인이 발생할 수 있다는 점
2) fetchResults 는 deprecated 되었다는 점
에서 직접 total 을 구한다(count 쿼리로)
Long count = queryFactory
.select(member.count())
.from(member)
.where(usernameEq(memberSearch.getMemberName()),
teamNameEq(memberSearch.getTeamName()),
teamQualityEq(memberSearch.getTeamQuality()),
ageLoe(memberSearch.getMaxAge()),
ageGoe(memberSearch.getMinAge()))
.fetchOne();
이를 PageImpl
에 넣어주면 우리가 평소에 쓰던 스프링 data jpa 를 이용해서 쓰던 그 방식을 사용할 수 있다.
하지만 PageableExecutionUtils.getPage
를 사용한다면
페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때 ,
마지막 페이지일때 (마지막 페이지이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때)
쿼리를 발생시키지 않으면서 조금이라도 더 count 쿼리를 줄여 효율적으로 사용이 가능하다.
public interface MemberRepository extends JpaRepository<Member,Long>,MemberRepositoryCustom {
List<Member> findMemberByUsername(String username);
}
위 처럼 extends
를 사용해서 우리가 만든 메서드를 MemberRepository
를 사용해서 쓸수 있게 한다.
아니면 너무 쿼리 DSL 에 종속되는 경우 걍 따로 QueryDsl 리포지토리
과 일반 스프링 Data jpa 리포지토리
를 분리해도 상관 없다.
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberSearchResponseWithSliceDto showMemberSearchWithSlice(MemberSearch memberSearch,
Pageable pageable){
Slice<MemberResponseWithTeamDto> memberSlice = memberRepository.showMemberWithSlice(memberSearch,pageable)
.map(member -> MemberResponseWithTeamDto.of(member));
return new MemberSearchResponseWithSliceDto(memberSlice.hasNext(),memberSlice.getContent());
}
public MemberSearchResponseWithPageDto showMemberSearchWithPage(MemberSearch memberSearch,
Pageable pageable){
Page<MemberResponseWithTeamDto> memberpage = memberRepository.showMemberWithPage(memberSearch,pageable)
.map(member -> MemberResponseWithTeamDto.of(member));
return new MemberSearchResponseWithPageDto(memberpage.getTotalElements(),memberpage.getContent(),memberpage.getTotalPages());
}
}
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping("/api/v1/members/s")
public MemberSearchResponseWithSliceDto memberSearchResponseWithSliceDto(MemberSearch memberSearch,
Pageable pageable){
return memberService.showMemberSearchWithSlice(memberSearch,pageable);
}
@GetMapping("/api/v1/members/p")
public MemberSearchResponseWithPageDto memberSearchResponseWithPageDto(MemberSearch memberSearch,
Pageable pageable){
return memberService.showMemberSearchWithPage(memberSearch,pageable);
}
}
그냥 반환해서 확인하면 된다 .
등등 모든 것이 가능하다.
List<Member>
로 리포지토리에서 데이터를 받으면
DTO
에 포함되지 않는 다른 것도 조회를 해야 한다.
이에 대해 성능이 나빠질 가능성이 있다 .
이런 문제를 해결하기 위해서
쿼리 Dsl 에서는 리포지토리에서 DTO 로 조회가 가능하다. (물론 spring data jpa 도)
List<MemberDto> result = queryFactor
.select(Projections.field(MemberDto.class,member.username.as("name"), member.age))
from(member)
.fetch()
//별칭이 다를 때 as("name")
을 붙여야 함
List<MemberDto> result = queryFactor
.select(Projections.constructor(MemberDto.class,member.username.as("name"), member.age))
from(member)
.fetch()
혹은 DTO 안에 생성자에 @QueryProjection
을 붙이면 더 깔끔하게 생성이 가능하나 DTO 가 QueryDsl 에 의존하게 되므로 나는 안쓸 것 같다.
이럴 때는 당연하게도 Team 과 연관되지 않은 Member 의 모습이 보이지 않는다 .
Init Service 의 마지막 에 아래 코드를 추가한다.
break;
case 4:
team=teamE;
break;
case 5:
team=teamF;
break;
}
Member member = Member.builder().name("member"+i).age(100-i).team(team).build();
em.persist(member);
}
Member member = Member.builder().name("member3000").age(3500).build();
em.persist(member);
}
마지막 member 는 team 에 대해 연관관계가 없어서 그냥 join 을 사용하면 아래와 같이 보이지 않는다.
이는 left join
으로 해결이 가능하다 .
@Override
public Page<Member> showMemberWithPage(MemberSearch memberSearch, Pageable pageable) {
List<Member> content = queryFactory
.selectFrom(member)
.leftJoin(member.team,team).fetchJoin()
.where(usernameEq(memberSearch.getMemberName()),
teamNameEq(memberSearch.getTeamName()),