<MemberJpaRepository.java>
public List<Member> findAll(){
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
public List<Member> findAll_querydsl(){
return queryFactory.selectFrom(member).fetch();
}
public List<Member> findByUsername(String username){
return em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", username)
.getResultList();
}
public List<Member> findByUsername_querydsl(String username){
return queryFactory.selectFrom(member).where(member.username.eq(username)).fetch();
}
<MemberTeamDto.java>
@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;
}
}
<MemberSearchCondition.java>
@Data
public class MemberSearchCondition {
private String username;
private String teamName;
private Integer ageGoe;
private Integer ageLoe;
}
ctrl + shift + enter
구문 자동 완성!!
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition){
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(condition.getUsername())) {
builder.and(member.username.eq(condition.getUsername()));
}
if (StringUtils.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.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")
))
.from(member)
.leftJoin(member.team, team)
.where(builder)
.fetch();
}
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();
}
// BooleanExpression 이 추후 composition 에 유리
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 num) {
return num != null ? member.age.goe(num) : null;
}
private BooleanExpression ageLoe(Integer num) {
return num != null ? member.age.loe(num) : null;
}
where 절... 너무 깔끔하잖아!! 좋아좋아 최고!
재사용 및 조립도 가능함!
application.yml 에 profiles -> active: local
추가
test 폴더에 application.yml 추가
다 똑같은데, profiles active 를 test
로 설정!
<InitMember.java> 파일 생성
@Profile("local")
@ComponentScan
@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 selectedTeam = i % 2 == 0 ? teamA : teamB;
em.persist(new Member("member" + i, i, selectedTeam));
}
}
}
}
@Profile("local")
은 profile local 에서만 실행하겠다는 어노테이션
@ComponentScan
은 spring 이 돌아갈떄, component scan 을 하라는 뜻.
@PostConstruct
는 시작할때 실행을 하겠다는 뜻
@Component
는 spring bean 에 component 로 지정
- 굳이
InitMemberService
를 만들어서 init 해주는 이유는 JPA 기능인@Transactional
은@PostConstruct
랑 함께 지정할 수가 없기 때문!!
<MemberController.java>
@GetMapping("/v1/members")
public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition){
return memberJpaRepository.search(condition);
}
호출하는 법
기존 <MemberJpaRepository.java>
Spring 데이터 JPA 를 활용한 <MemberRepository.java>
Query dsl 은 사용자 정의 리포지토리를 통해 구현한다.
스프링 데이터 JPA 에서 했던 방식.
<MemberRepositoryCumstom.java> 파일 생성
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
<MemberRepositoryImpl.java> 파일 생성
public class MemberRepositoryImpl implements MemberRepositoryCustom{
private final JPAQueryFactory queryFactory;
public MemberRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
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();
}
...
// usernameEq, teamNameEq, ageGoe, ageLoe 함수
}
<MemberRepository.java> 파일
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username);
}
MemberRepositoryCustom
이라는 interface를 만들어,MemberRepositoryImpl
로 구현
- 이때 구현체는
JpaRepository
를 상속받는 리포지토리 뒤에impl
붙은 이름과 같아야함
(MemberRepository
,MemberRepositoryImpl
)MemberRepository
는JpaRepository
와<해당 인터페이스>
를 상속받으면 됨.
조회 쿼리가 너무 복잡한 경우.
MemberRepository
에 합치는 것이 아니라,
새로운MemberQueryRepository
를 만드는 것도 좋음.
특화된 기능은 따로 만드는 것도 좋음!! 합치는 것이 무조건 정답이 아님.
- 비즈니스, 설계에 따라 유동적으로 도입할 줄 아는 유연성을 가질 것!!
Page
, Pageable
을 활용해보기!public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
/*추가*/ Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
/*추가*/ Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
기존 쿼리문에
offset
과limit
를 추가 해주는 방식으로 pageable 구현이 가능하다!!
PageImpl
구현체를 사용하여 Page 타입으로 반환 가능!
테스트 코드
@Test
public void searchPageSimpleTest() throws Exception{
// given
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member m1 = new Member("m1", 10, teamA);
Member m2 = new Member("m2", 20, teamA);
Member m3 = new Member("m3", 15, teamB);
Member m4 = new Member("m4", 25, teamB);
Member m5 = new Member("m5", 30);
Member m6 = new Member("m6", 40);
em.persist(m1);
em.persist(m2);
em.persist(m3);
em.persist(m4);
em.persist(m5);
em.persist(m6);
em.flush();
em.clear();
// when
MemberSearchCondition condition = new MemberSearchCondition();
PageRequest pageRequest = PageRequest.of(0, 3);
Page<MemberTeamDto> result = memberRepository.searchPageSimple(condition, pageRequest);
// then
assertThat(result.getSize()).isEqualTo(3);
assertThat(result.getContent()).extracting("username").containsExactly("m1", "m2", "m3");
}
이 방법의 경우, getTotal()
을 통해 count 를 불러오는데, 이때 count query에 특정 쿼리를 지정해주면 좋다.
result 를 그냥 fetch
를 활용하여 그대로 받아오고,
fetchCount
를 활용해서, 최적화된 count 함수를 실행함.
핵심은 count 를 직접 지정해주어서, count와 상관 없는 쿼리가 포함되는 것을 막을 수 있음 (성능 최적화)
count 가 0일때는 search 쿼리를 날리지 않는 다던지 하는
데이터가 얼마 없을때는 그냥...fetchResult
써라...
JPAQuery<Member> countQuery = queryFactory
.selectFrom(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
);
return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
스프링데이터 JPA 에서 PageableExecutionUtils
로 그 기능을 제공해준다.
세번째 input 으로는 page count 가 필요할때 실행할 함수를 넣어주면 된다!
if 문 넣어서 직접 해도 되긴함!!
이런식으로 controller 에서 불러와 사용하면 됨!
여러개 가 있기는 하지만, 복잡한 실무에서 사용하기는 좀 애매함... 참고만!!
QuerydslPredicateExecutor
1 - query dsl 코드를 인자로 넘길 수 있음
2 - left join 지원 안해줌.
3 - 서비스, 컨트롤러 계층에 query 문이 들어감
Querydsl Web 지원
1 - @QuerydslPredicate
를 활용해서 파라미터를 편하게 받을 수 있음
2 - 조건문이 한정적임 / custom 하려면 너무 복잡해짐
3 - 마찬가지로 상위 계층에 query 관련 function 이 포함됨.
QuerydslRepositorySupport
1 - 추상클래스임 (특정 리포지토리에 상속시키면 됨)
2 - queryFactory 없이, 구현된 특정 메소드들을 활용하여 쉽게 메소드 짤 수 있음
김영한 강사님의.
QuerydslRepositorySupport
한계 극복 클래스 직접 제작!!
@Repository
public abstract class Querydsl4RepositorySupport {
private final Class domainClass;
private Querydsl querydsl;
private EntityManager entityManager;
private JPAQueryFactory queryFactory;
public Querydsl4RepositorySupport(Class<?> domainClass) {
Assert.notNull(domainClass, "Domain class must not be null!");
this.domainClass = domainClass;
}
@Autowired
public void setEntityManager(EntityManager entityManager) {
Assert.notNull(entityManager, "EntityManager must not be null!");
JpaEntityInformation entityInformation =
JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);
SimpleEntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE;
EntityPath path = resolver.createPath(entityInformation.getJavaType());
this.entityManager = entityManager;
this.querydsl = new Querydsl(entityManager, new
PathBuilder<>(path.getType(), path.getMetadata()));
this.queryFactory = new JPAQueryFactory(entityManager);
}
@PostConstruct
public void validate() {
Assert.notNull(entityManager, "EntityManager must not be null!");
Assert.notNull(querydsl, "Querydsl must not be null!");
Assert.notNull(queryFactory, "QueryFactory must not be null!");
}
protected JPAQueryFactory getQueryFactory() {
return queryFactory;
}
protected Querydsl getQuerydsl() {
return querydsl;
}
protected EntityManager getEntityManager() {
return entityManager;
}
protected <T> JPAQuery<T> select(Expression<T> expr) {
return getQueryFactory().select(expr);
}
protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) {
return getQueryFactory().selectFrom(from);
}
protected <T> Page<T> applyPagination(Pageable pageable,
Function<JPAQueryFactory, JPAQuery> contentQuery) {
JPAQuery jpaQuery = contentQuery.apply(getQueryFactory());
List<T> content = getQuerydsl().applyPagination(pageable,
jpaQuery).fetch();
return PageableExecutionUtils.getPage(content, pageable,
jpaQuery::fetchCount);
}
protected <T> Page<T> applyPagination(Pageable pageable,
Function<JPAQueryFactory, JPAQuery> contentQuery, Function<JPAQueryFactory,
JPAQuery> countQuery) {
JPAQuery jpaContentQuery = contentQuery.apply(getQueryFactory());
List<T> content = getQuerydsl().applyPagination(pageable,
jpaContentQuery).fetch();
JPAQuery countResult = countQuery.apply(getQueryFactory());
return PageableExecutionUtils.getPage(content, pageable,
countResult::fetchCount);
}
}
이런 식으로 기존에 있던게 불편하다면, 중간에 함수들을 약간 변경해서 내 맛대로 사용해도 되는 거였음!!