메인 프로젝트 (9) 랭킹 - 동적 쿼리 & 프로젝션

InSeok·2022년 12월 10일
0

프로젝트

목록 보기
13/13

랭킹 조회

  • 랭킹 페이지에서 실시간으로 유저들의 랭킹을 확인할 수 있으며, 랭커들의 운동스타일, 신체 조건, 경력, 포인트 및 챌린지 대결중인지 여부도 확인가능합니다.
  • Member 엔티티는 아래와 같이 많은 정보들을 담고있는데, 랭킹페이지를 조회할때마다 해당 페이지의 member들의 모든 정보를 불러온다면, 리소스 낭비가 될것입니다.
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;
    private String password;
    private String username;
    private String gender;
    private String job;
    private String address;
    private Integer age;
    private Integer height;
    private Integer weight;
    private Integer split;
    private Integer period;
    @Embedded
    private MemberActivity memberActivity;
  • 따라서, Querydsl의 Projection 을 활용하여 , entity 전체를 가져오는 것이아니라 조회 대상을 지정해 원하는 값만 조회하였습니다.

프로젝션(Projection)

  • select절에서 어떤 컬럼들을 조회할지 대상을 지정하는 것
  • 프로젝션이 두개이상일경우, Tuple이나 DTO로 값을 조회 할 수있는데, 이번에는 결과를 DTO로 조회하여 반환하였다.

Querydsl 빈 생성(Bean population)

  • 결과를 DTO로 반환할 때 사용하며, 3가지 방식이 있다.
  1. 프로퍼티 접근

    • setter(bean)를 통해 데이터를 인젝션 해주며 기본 생성자가 무조건 필요
    List<MemberDto> result = queryFactory
              .select(Projections.bean(MemberDto.class,
                      member.username,
                      member.age))
              .from(member)
              .fetch();
  2. 필드 직접 접근

    • 필드에 값을 딱 꽂아주기 때문에 setter와 기본생성자가 필요없다.
    List<MemberDto> result = queryFactory
              .select(Projections.fields(MemberDto.class,
                      member.username,
                      member.age))
              .from(member)
              .fetch();
  3. 생성자 사용

    • 값을 넘길 때생성자와 순서가 맞아야 데이터를 불러오며, @AllArgsConstructor 필요
    List<MemberDto> result = queryFactory
              .select(Projections.constructor(MemberDto.class,
                      member.username,
                      member.age))
              .from(member)
              .fetch()
     }

생성자 + @QueryProjection

  • 생성자 방식은 @QueryProjection을 지원하며, DTO도 Q파일로 생성해준다.
  • QueryProjection은 컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법이다.
@Data
public class MemberDto {
 private String username;
 private int age;

 public MemberDto() {
 }

 @QueryProjection
 public MemberDto(String username, int age) {
 this.username = username;
 this.age = age;
 }
}

랭킹 검색

  • 랭킹페이지에서는 유저들의 운동스타일, 신체 조건, 경력과 같은 다양한 조건들을 설정하여 랭킹을 조회할 수 있어 나와 운동능력이 비슷한 유저를 빠르게 찾고 대결을 신청할 수 있다.

동적 쿼리

  • 상황에 따라 다른 문법의 SQL을 적용하는 것
  • QueryDsl을 활용하여, DB에서 값을 조회할 때 상황에따라 조건을 동적으로 바꿔서 조회한다.
  • 이번 프로젝트에서는 BooleanExpression을 활용하여 구현하였다.

1. BooleanBuilder

  • where문의 조건을 한눈에 보기 어렵다.
  • 쿼리 형태를 예측하기가 어렵다
private List<Member> searchMember1(String usernameCond, Integer ageCond) {
BooleanBuilder builder = new BooleanBuilder();
if (usernameCond != null) {
builder.and(member.username.eq(usernameCond));
}
if (ageCond != null) {
builder.and(member.age.eq(ageCond));
}
return queryFactory
.selectFrom(member)
.where(builder)
.fetch();
}

2. BooleanExpression (Where 다중 파라미터)

  • BooleanExpression은 where절에서 사용할 수 있는 값인데, 아래의 Querydsl기능들을 활용하여 동적쿼리를 구현할 수 있다.
  • where()에 null이 들어오면 무시한다.
  • where()에 , 을 and 조건으로 사용한다.
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
return queryFactory
.selectFrom(member)
.where(usernameEq(usernameCond), ageEq(ageCond))
.fetch();
}

// BooleanExpression
//username의 값이 존재하면 조건 추가, null or 빈문자("")일 경우 null 반환
private BooleanExpression usernameEq(String usernameCond) {
return usernameCond != null ? member.username.eq(usernameCond) : null;
}
//age의 값이 존재하면 조건 추가, null or 빈문자("")일 경우 null 반환
private BooleanExpression ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null;
}

장점

  1. where() 메서드의 인자로 사용되는 메서드명을 보고 어렵지 않게 쿼리를 파악할 수 있다.
  2. BooleanExpression 객체들을 조립할 수 있다.
  3. 재사용 가능
  4. NullPointrException을 방지

실제코드

1. @QueryProjection 활용해 DB에서 필요한 칼럼만 조회하는 DTO 생성

@Getter
public class RankingDto {

    private Long memberId;
    private String userName;
    private String profileImage;
    private Integer height;
    private Integer weight;
    private Double point;
    private Integer period;
    private Long challengeId;

    @QueryProjection
    public RankingDto(Long memberId, String userName, String profileImage, Integer height, Integer weight,
                      Double point, Integer period, Long challengeId) {
        this.memberId = memberId;
        this.userName = userName;
        this.profileImage = profileImage;
        this.height = height;
        this.weight = weight;
        this.point = point;
        this.period = period;
        this.challengeId = challengeId;
    }

2. 동적 쿼리에 사용할 검색 조건 클래스 생성

public class RankingCondition {
    //분할, 키, 몸무게, 경력
    private Integer split;
    private Integer heightGoe;
    private Integer heightLt;
    private Integer weightGoe;
    private Integer weightLt;
    private Integer periodGoe;
    private Integer periodLt;

    public RankingCondition(Integer split, Integer heightGoe, Integer heightLt, Integer weightGoe,
                            Integer weightLt, Integer periodGoe, Integer periodLt) {
        this.split = split;
        this.heightGoe = heightGoe;
        this.heightLt = heightLt;
        this.weightGoe = weightGoe;
        this.weightLt = weightLt;
        this.periodGoe = periodGoe;
        this.periodLt = periodLt;
    }

}

3. Repository에 Where 다중 파라미터를 이용한 동적 쿼리 메서드 구현

public List<RankingDto> rankingList(RankingCondition condition, Pageable pageable) {
   return jpaQueryFactory
            .select(new QRankingDto(
member.id,
member.username,
member.profileImage.path,
member.height,
member.weight,
memberActivity.point,
member.period,
member.challenge.id
            ))
            .from(member)
            .where(splitEq(condition.getSplit()),
                    heightGoe(condition.getHeightGoe()),
                    heightLt(condition.getHeightLt()),
                    weightGoe(condition.getWeightGoe()),
                    weightLt(condition.getWeightLt()),
                    periodGoe(condition.getPeriodGoe()),
                    periodLt(condition.getPeriodLt()))

// 분할 일치 
    private BooleanExpression splitEq(Integer split) {
        return isEmpty(split) ? null : member.split.eq(split);
    }
    // 키 이상
    private BooleanExpression heightGoe(Integer heightGoe) {
        return isEmpty(heightGoe) ? null : member.height.goe(heightGoe);
    }
    // 키 미만
    private BooleanExpression heightLt(Integer heightLt) {
        return isEmpty(heightLt) ? null : member.height.lt(heightLt);
    }
    // 몸무게 이상
    private BooleanExpression weightGoe(Integer weightGoe) {
        return isEmpty(weightGoe) ? null : member.weight.goe(weightGoe);
    }
    // 몸무게 미만
    private BooleanExpression weightLt(Integer weightLt) {
        return isEmpty(weightLt) ? null : member.weight.lt(weightLt);
    }  
    // 경력 이상
    private BooleanExpression periodGoe(Integer periodGoe) {
        return isEmpty(periodGoe) ? null : member.period.goe(periodGoe);
    }
    // 경력 미만
    private BooleanExpression periodLt(Integer periodLt) {
        return isEmpty(periodLt) ? null : member.period.lt(periodLt);
    }
profile
백엔드 개발자

0개의 댓글