순수 JPA와 Querydsl Repository
package study.querydsl.repository;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import study.querydsl.entity.Member;
import study.querydsl.entity.QMember;
import javax.persistence.EntityManager;
import java.util.*;
import static study.querydsl.entity.QMember.*;
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private final EntityManager em;
private final JPAQueryFactory queryFactory;
public void save(Member member){
em.persist(member);
}
public Optional<Member> findById(Long id){
return Optional.ofNullable(em.find(Member.class ,id));
}
public List<Member> findAll_Querydsl() {
return queryFactory
.selectFrom(member)
.fetch();
}
public List<Member> findByUsername_Querydsl(String username){
return queryFactory
.selectFrom(member)
.where(member.username.eq(username))
.fetch();
}
}
Spring Data JPA 없이 JPA와 querydsl을 사용한 예시이다.
참고로, @RequiredArgsConstructor로 JPAQueryFactory에 넣어줄 수 있는 이유는 Spring Bean으로 등록해놨기 때문이다. 아래로 예시를 들어주겠다.
@SpringBootApplication
public class QuerydslApplication {
public static void main(String[] args) {
SpringApplication.run(QuerydslApplication.class, args);
}
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em){
return new JPAQueryFactory((em));
}
}
동적 쿼리와 성능 최적화 조회 - builder 사용
MemberTeamDto 생성
package study.querydsl.dto;
import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;
@Data
public class MemberTeamDto {
private Long memberId;
private String username;
private int age;
private Long teamId;
private String teamName;
@QueryProjection
public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) {
this.memberId = memberId;
this.username = username;
this.age = age;
this.teamId = teamId;
this.teamName = teamName;
}
}
Member와 Team을 함께 반환해줄 dto 생성
MemberSearchCondition 생성
package study.querydsl.dto;
import lombok.Data;
@Data
public class MemberSearchCondition {
// 회원명, 팀명, 나이 (ageGoe, ageLoe)
private String username;
private String teamName;
private Integer ageGoe;
private Integer ageLoe;
}
파라미터 조건에따라 MemberTeamDto를 반환해줄 파라미터 안에 넣을 클래스
MemberJpaRepository 코드 추가
public class MemberJpaRepository {
...
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition memberSearchCondition){
BooleanBuilder builder = new BooleanBuilder();
if(StringUtils.hasText(memberSearchCondition.getUsername())) {
builder.and(member.username.eq(memberSearchCondition.getUsername()));
}
if(StringUtils.hasText(memberSearchCondition.getTeamName())) {
builder.and(team.name.eq(memberSearchCondition.getTeamName()));
}
if(memberSearchCondition.getAgeGoe() != null){
builder.and(member.age.goe(memberSearchCondition.getAgeGoe()));
}
if(memberSearchCondition.getAgeLoe() != null){
builder.and(member.age.loe(memberSearchCondition.getAgeLoe()));
}
return queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")
))
.from(member)
.leftJoin(member.team, team)
.where(builder)
.fetch();
}
}
BooleanBuilder를 사용해서 조건을 만들어주어서 where문 안에 넣는 예시이다.
참고로, StringUtils.hasText()는 null이나 ""빈 값 모두 false로 걸러주는 기능을 해준다.
음.. 확실히 이렇게 보니 가독성이 떨어지는 것 같긴하다..
그렇다면, where절은 조금 더 깔끔하겠지?!
동적 쿼리와 성능 최적화 조회 - Where절 파라미터 사용
MemberJpaRepository 코드 추가
public class MemberJpaRepository {
...
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")
))
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamnameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetch();
}
private BooleanExpression usernameEq(String username) {
return StringUtils.hasText(username)? member.username.eq(username) : null;
}
private BooleanExpression teamnameEq(String teamName) {
return StringUtils.hasText(teamName)? team.name.eq(teamName) : null;
}
private BooleanExpression ageGoe(Integer ageGoe) {
return (ageGoe == null)? null : member.age.goe(ageGoe);
}
private BooleanExpression ageLoe(Integer ageLoe) {
return (ageLoe == null)? null : member.age.loe(ageLoe);
}
}
이렇게 각각 기능들을 메소드로 분리함으로써 가독성도 높아지고, 재사용성도 생긴다. 개인적으로 BooleanBuilder보다 where절이 객체지향 프로그래밍에 맞는 코드인 것 같다.
조회 API 컨트롤러 개발
사전 준비
main에 있는 application.yml을 test에도 추가해줍니다. (resources 파일도 추가해야 함)
그리고 설정을 아래와 같이 local부분만 다르게 test로 작성해줍니다. (main도 profiles: active: local 추가)
spring:
profiles:
active: local // main (test 경우에는 local이 아닌 test)
datasource:
url: jdbc:h2:tcp://localhost/~/querydsl
...
그 후 InitMember 클래스 추가 (애플리케이션 로딩 시점에 데이터를 추가하기 위한 클래스 입니다. 테스트 용도)
package study.querydsl.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import study.querydsl.entity.Member;
import study.querydsl.entity.Team;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@Profile("local")
@Component
@RequiredArgsConstructor
public class InitMember {
private final InitMemberService initMemberService;
@PostConstruct
public void init() {
initMemberService.init();
}
@Component
static class InitMemberService {
@PersistenceContext private EntityManager em;
@Transactional
public void init() {
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
for(int i = 0; i < 100; i++){
Team selectTeam = i % 2 == 0? teamA : teamB;
em.persist(new Member("member"+i, i, selectTeam));
}
}
}
}
InitMemberService를 따로 만들어준 이유는 @PostConstruct와 @Transactional을 함께 사용하지 못하기 때문에 분리하기 위해 만들어 주었다. @Profiles("local")은 이전에 application.yml에 설정해주었던 profiles 값이 Local인 경우에만 구동시켜준다.
사전 준비는 끝났다. 게임을 시작해보자.
MemberController 생성
package study.querydsl.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import study.querydsl.dto.MemberSearchCondition;
import study.querydsl.dto.MemberTeamDto;
import study.querydsl.repository.MemberJpaRepository;
import java.util.List;
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberJpaRepository memberJpaRepository;
@GetMapping("/v1/members")
public List<MemberTeamDto> seachMemberV1(MemberSearchCondition condition) {
return memberJpaRepository.search(condition);
}
}
만들고 주소/v1/members로 검색하면 이전에 넣어주었던 데이터들이 나온다. (웹에서 접근하면 보기 힘들기 때문에 postman과 같은 api툴을 사용하기를 바란다!)
위와같이 파라미터 조건을 추가하면 조건에 맞게 조회된 데이터를 확인할 수 있다.
참고로 아무런 파라미터를 설정하지 않는다면 모든데이터가 조회되기 때문에, 사용할 때 각별히 주의하자!!!!
출처
'spring > 인프런 강의 정리' 카테고리의 다른 글
[Querydsl] 6. Spring Data JPA가 제공하는 Querydsl 기능 (0) | 2022.05.20 |
---|---|
[Querydsl] 5. 실무 활용 - 스프링 데이터 JPA와 Querydsl (+페이징) (0) | 2022.05.20 |
[Querydsl] 3. 중급 문법 (0) | 2022.05.18 |
[Querydsl] 2. 기본 문법 (0) | 2022.05.17 |
[Querydsl] 1. 프로젝트 세팅 (0) | 2022.05.17 |