MemberJpaRepository
@Repository
public class MemberJpaRepository {
private final EntityManager em;
private final JPAQueryFactory jpaQueryFactory;
public MemberJpaRepository(EntityManager em) {
this.em = em;
this.jpaQueryFactory = new JPAQueryFactory(em);
}
장점 : 테스트 할때 EntityManager만 주입해주면된다.
QueryDsl3Application
@SpringBootApplication
public class QueryDsl3Application {
public static void main(String[] args) {
SpringApplication.run(QueryDsl3Application.class, args);
}
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em){
return new JPAQueryFactory(em);
}
}
MemberJpaRepository
@Repository
public class MemberJpaRepository {
private final EntityManager em;
private final JPAQueryFactory jpaQueryFactory;
public MemberJpaRepository(EntityManager em,JPAQueryFactory jpaQueryFactory) {
this.em = em;
this.jpaQueryFactory = jpaQueryFactory;
}
이것을 @RequiredArgsConstructor 을 사용하면 아래와 같이 코드가 줄어든다.
MemberJpaRepository
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private final EntityManager em;
private final JPAQueryFactory jpaQueryFactory;
장점 : 코드가 줄어든다.
단점 : 별도의 스프링빈 등록이 필요하고, 테스트 때 조금 첫번째 방식보단 복잡하다.
참고: 동시성 문제는 걱정하지 않아도 된다. 왜냐하면 여기서 스프링이 주입해주는 엔티티 매니저는 실제 동 작 시점에 진짜 엔티티 매니저를 찾아주는 프록시용 가짜 엔티티 매니저이다. 이 가짜 엔티티 매니저는 실제 사용 시점에 트랜잭션 단위로 실제 엔티티 매니저(영속성 컨텍스트)를 할당해준다.
MemberTeamDto - 조회 최적화용 DTO 추가
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;
} }
@QueryProjection 을 추가했다.
QMemberTeamDto 를 생성하기 위해 ./gradlew compileQuerydsl 을 한번 실행하자.
참고: @QueryProjection 을 사용하면 해당 DTO가 Querydsl을 의존하게 된다. 이런 의존이 싫으면, 해 당 에노테이션을 제거하고, Projection.bean(), fields(), constructor() 을 사용하면 된다.
회원 검색 조건
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;
}
동적쿼리 - Builder + @QueryProjection 사용
//Builder 사용
//회원명, 팀명, 나이(ageGoe, ageLoe)
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) {
BooleanBuilder builder = new BooleanBuilder();
//StringUtils.hasText() import stirng.framework -> null, "" 값d아닌 값을 boolean 값으로 반환
if (hasText(condition.getUsername())) {
builder.and(member.username.eq(condition.getUsername()));
}
if (hasText(condition.getTeamName())) {
builder.and(team.name.eq(condition.getTeamName()));
}
if (condition.getAgeGoe() != null) {
builder.and(member.age.goe(condition.getAgeGoe()));
}
if (condition.getAgeLoe() != null) {
builder.and(member.age.loe(condition.getAgeLoe()));
}
return queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.from(member)
.leftJoin(member.team, team)
.where(builder)
.fetch();
}
QMemberTeamDto 는 생성자를 사용하기때문에 필드 이름을 맞추지 않아도 된다. 타입을 맞추기 때문이다. 만약 필드 또는 프로퍼티 접근 방식이라면
member.id.as("memberId") 이라고 적어야 한다.
참고 ) 동적쿼리 - Builder + 생성자 DTO 사용
return jpaQueryFactory
.select
(Projections.constructor(
MemberTeamDto.class,member.id,member.username,member.age,team.id,team.name))
.from(member)
.leftJoin(member.team,team)
.where(builder)
.fetch();
}
//회원명, 팀명, 나이(ageGoe, ageLoe)
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
}
member.id,
member.username,
member.age,
team.id,
team.name))
.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 isEmpty(username) ? null : member.username.eq(username);
}
private BooleanExpression teamNameEq(String teamName) {
return isEmpty(teamName) ? null : team.name.eq(teamName);
}
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);
}
where 절에 파라미터 방식을 사용하면 조건 재사용 가능
//where 파라미터 방식은 이런식으로 재사용이 가능하다.
public List<Member> findMember(MemberSearchCondition condition) {
return queryFactory
.selectFrom(member)
}
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetch();
편리한 데이터 확인을 위해 샘플 데이터를 추가
샘플 데이터 추가가 테스트 케이스 실행에 영향을 주지 않도록 다음과 같이 프로파일을 설정
-테스트 케이스와 샘플케이스를 분리하기
spring:
profiles:
active: local
테스트는 기존 application.yml을 복사해서 다음 경로로 복사하고, 프로파일을 test로 수정하자
src/test/resources/application.yml
spring:
profiles:
active: test
이렇게 분리하면 main 소스코드와 테스트 소스 코드 실행시 프로파일을 분리할 수 있다.
샘플 데이터 추가
@Component
@RequiredArgsConstructor
@Profile("local")
public class InitData {
private final InitMemberService initMemberService;
@PostConstruct // @PostConstruct 와 @Transactional 함께 사용할 수 없어 분리해서 만듬
public void init(){
initMemberService.init();
}
@Component
static class InitMemberService{
@PersistenceContext
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 selectedTeam = i % 2 == 0 ? teamA : teamB ;
em.persist(new Member("member"+i,i,selectedTeam));
}
}
}
}
조회 컨트롤러
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberJpaRepository memberJpaRepository;
@GetMapping("/v1/members")
public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition)
{
return memberJpaRepository.search(condition);
}
}
assertThat(builder).extracting("username").containsExactly("member4")
extracting("username"):
extracting 메서드는 객체에서 특정 필드나 속성을 추출하여 검증할 수 있도록 해줍니다. 이 경우에는 builder 객체의 username 필드를 추출하여 검증합니다. 추출된 필드 값들로 새로운 리스트가 생성됩니다.
즉 ! 추출된 필드 값들로 리스트를 반환한다.
containsExactly("member4"):
containsExactly 메서드는 추출된 필드 값들의 순서와 내용이 주어진 값들과 정확히 일치하는지를 검증합니다. 이 경우에는 추출된 username 필드 값들이 "member4" 하나만을 포함하고 있어야 합니다.
참고
김영한 QueryDsl 강의 자료