[JPA + Querydsl]코드 스니펫

zzarbttoo·2022년 1월 4일
1

DB/Mybatis/JPA

목록 보기
4/7

2021년이 가기 전 Mybatis 인생을 탈피하고자(!)
인프런에 있는 김영한님의 강의 두개를 켠왕했다( 실전 jpa 강의, querydsl 강의)

(2021년 마지막 날 태우는 중.jpg)

querydsl은 jpa 동적쿼리인데 이걸 공부한 이유는 이전에 jpa로 모듈을 짜놨는데 테이블 이름이 계속 변하는(!) 엄청난 상황을 만나 결국 mybatis를 이용해 프로그램을 다시 짰던 트라우마(?)가 있어서
혹시 jpa 전용 동적쿼리를 쓰면 괜찮지 않을까 해서 공부해보았다
(결론부터 얘기하면 querydsl도 entity 기반이여서 저 경우에는 그냥 JDBC나 mybatis 사용하는게 나을 것 같다)

비록 궁금증을 해결하지는 못했지만 강의를 들으며 JPA와 querydsl의 편리함에 대해 알 수 있었다

필요한 부분을 정리해서 스니펫으로 활용해보도록 하겠다 👩‍🏫
이 곳에는 간단하게만 적을 것이고 좀 더 깊은 내용에 대해서는 각각 따로 정리할 것이다


fetchJoin

@ManyToOne(fetch = FetchType.LAZY)으로 지연로딩을 하면 객체를 조회할 때 join된 객체를 바로 불러오는 것이 아니라 프록시 객체로 대체를 해놓고,
필요할 때 직접 select 해오게 된다
그래서 연관객체를 한 번에 불러오고 싶다면 fetch join을 하는 것이 성능 최적화하는데 도움이 된다(1 + N 문제 해결)


jpql(Spring Data JPA)

@Query("select m from Member m left join fetch m.team")
    List<Member> findMemberFetchJoin();

Querydsl

@Autowired
EntityManager em;

JPAQueryFactory queryFactory = new JPAQueryFactory(em);

...

        //fetch join 할 때는 초기화 하고 시작하는 것이 좋음
        em.flush();
        em.clear();

        //기본적으로는 team이 Lazy로 되어있다
        Member findMember = queryFactory.selectFrom(QMember.member)
                .join(member.team, Qteam.team).fetchJoin()
                .where(QMember.member.username.eq("member1"))
                .fetchOne();

        boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam()); //초기화 된 엔티티 여부
        assertThat(loaded).as("페치 조인 적용").isTrue();

  • fetch join 시 초기화 하고 시작(flush, clear)
  • 필요한 Entity만 불러오고자 한다면 그냥 join 이용

DTO 반환


Spring Data JPA

@Query("select new com.zzarbttoo.datajpa.dto.MemberDTO(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDTO> findMemberDTO();
  • DTO로 조회할 때 new operation을 사용해야한다
  • 패키지 이름을 다 써줘야 한다(생성자로 조회하는 것처럼)

Querydsl

1) setter

 List<MemberDTO> result = queryFactory.select(
 Projections.bean(MemberDTO.class,member.username,member.age))
  		.from(member)
            	.fetch();
  • 기본 생성자가 없으면 에러가 나게 된다
  • Projections.bean 이용

2) field

List<MemberDTO> result = queryFactory.select(Projections.fields(MemberDTO.class
                , member.username
                , member.age))
                .from(member)
                .fetch();
  • Projections.fields 이용
  • 필드 접근제어자가 private이여도 알아서 설정이 된다(?)
  • 필드명이 같아야 한다

3) constructor

List<MemberDTO> result = queryFactory.select(Projections.constructor(MemberDTO.class
				, member.username, member.age))
                .from(member)
                .fetch();
  • 순서가 constructor 순서와 같아야한다
  • 생성자는 타입을 보고 들어가기 때문에 필드명이 달라도 상관 없다
  • 순서가 이상하게 들어갈 경우 runtime 오류 발생

4) field명이 다를 때 DTO select

QMember memberSub = new QMember("memberSub");

List<UserDTO> result = queryFactory.select(Projections.fields(UserDTO.class
                , member.username.as("name") //다른 field명을 직접 명시해줘야한다
                //, member.age))
                , ExpressionUtils.as(JPAExpressions
                        .select(memberSub.age.max()) //서브쿼리를 이용해서 출력한다 가정
                        .from(memberSub), "age") //두번째 파라미터를 age로 별칭을 만들었다
                ))
                .from(member)
                .fetch();
  • 원래 이렇게 출력하는 것은 아니고 Subquery를 출력해보기 위한 예제
  • 다른 필드 명은 직접 명시를 해주면 된다

5) Query Projection

@Data //기본 생성자는 안만들어준다
@NoArgsConstructor
public class MemberDTO {

    private String username;
    private int age;

    @QueryProjection //QueryProjection annotation 생성 후 querydsl compile 해줘야 한다
    public MemberDTO(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

이렇게 DTO 생성자에 QueryProjection annotation 생성한 후 querydsl compile을 하고
다음과 같이 사용 가능

List<MemberDTO> result = queryFactory.select(new QMemberDTO(member.username, member.age))
                .from(member)
                .fetch();
  • MemberDTO가 querydsl에 의존성을 가지게 되고, 그럼 querydsl을 사용하지 않으면 문제가 생긴다
  • service, controller에도 내부 로직이 노출되게 된다
  • 아키텍처적으로 지저분하지만 실용적일 수는 있다

Paging

JPA

    public List<Member> findByPage(int age, int offset, int limit){
        return em.createQuery("select m from Member m where m.age = :age order by m.username desc")
                .setParameter("age", age)
                .setFirstResult(offset) //어디서부터 가져올 것인가
                .setMaxResults(limit) //몇개 넘길 것인가
                .getResultList();
    }

Spring Data JPA

    @Query(value = "select m from Member m left join m.team t",
            countQuery = "select count(m.username) from Member m")
    Page<Member> findByAge(int age, Pageable pageable);
  • count 쿼리는 join을 할 필요가 없기 때문에 분리해서 사용하는 것이 좋다
  • sorting 조건 또한 복잡하면 따로 빼서 짜면 된다
  • page.map(memberEntity -> new DTO(memberEntity.getId, ...) 이러한 방식으로 Entity를 DTO로 변환도 가능하다
  • 혹은 page.map(DTO::new)로 변환 가능
  • Spring data에서 제공해주는 page는 0부터 시작한다
참고로 PageRequest 는 다음과 같이 작성도 가능하다(Test 할 때 유용~)
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

Querydsl

     List<MemberTeamDTO> content = 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())
                )
                //.orderBy()
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        JPAQuery<Member> countQuery = queryFactory.select(member).from(member)
                .where(usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                );//.fetchCount();


        //return new PageImpl<>(content, pageable, total);

        //count를 생략할 수 있을 때 생략하도록 한다
        //return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
        return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);
  • count 쿼리를 분리해서 최적화 가능(join 필요 없음)
  • count 쿼리가 필요하지 않을 때는 실행하지 않는다
    (ex 한 페이지보다 데이터가 적을 때 등)

subQuery

Querydsl

QMember memberSub = new QMember("memberSub");

List<Member> result = queryFactory.selectFrom(member)
                	.where(member.age.goe(select(memberSub.age.avg())
                        .from(memberSub)
                ))
                .fetch();
                
 List<Member> result = queryFactory.selectFrom(member)
                .where(member.age.in(select(memberSub.age)
                        .from(memberSub).where(memberSub.age.gt(10))
                ))
                .fetch();
                

List<Tuple> result = queryFactory.select(member.username,
		select(memberSub.age.avg()).from(memberSub))
                .from(member)
                .fetch();
  • subquery의 alias는 밖의 alias와 겹치면 안된다
  • 따라서 다른 alias를 가진 Q entity를 만들어준다
  • JPA JPQL, querydsl은 from절에서 subquery(인라인뷰) 지원하지 않는다
from 절 서브쿼리 해결방안
1. 서브 쿼리 -> join(가능한 상황이 있고 불가능한 상황이 있다)
2. 어플리케이션에서 쿼리를 2번 분리해서 실행한다
3. nativaSQL을 사용한다

     
정말 복잡한 쿼리는 쪼개서 하면 더 나을 수 있다
SQL은 정말 최소한의 데이터를 호출/grouping 하는 것에 집중해서 쓰면 된다

bulkUpdate

JPA

public int bulkAgePlus(int age){
        int resultCount = em.createQuery("update Member m set m.age = m.age + 1 " +
                "where m.age >= :age")
                .setParameter("age", age)
                .executeUpdate(); 
  • executeUpdate를 해야 업데이트 갯수가 나온다

Spring Data JPA

1) flush, clear

@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

라고 repository에 선언하고 아래와 같이 호출

int resultCount = memberRepository.bulkAgePlus(20);

entityManager.flush();
entityManager.clear();
  • 벌크 연산 이후에는 꼭 영속성 컨텍스트를 날려야한다

2) modifying 설정

@Modifying(clearAutomatically = true) //자동으로 clear 과정을 해준다
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

혹은 @Modifying 설정을 해서 자동 clear을 할 수 있다

int resultCount = memberRepository.bulkAgePlus(20);

Querydsl

long count = queryFactory.update(member)
              .set(member.username, "비회원")
              .where(member.age.lt(28)) //28 이하면 이름을 비회원으로 바꿈
              .execute();


//bulk 연산에서는 초기화 필요(영속성 컨텍스트 날림)
em.flush();
em.clear();


queryFactory.update(member)
//.set(member.age, member.age.add(1)) //마이너스 하고 싶으면 add(-1)
.set(member.age, member.age.multiply(2)) //마이너스 하고 싶으면 add(-1)
.execute(); //모든 회원의 나이 한살 더하기

queryFactory.delete(member)
	.where(member.age.gt(18))
	.execute();

dynamic query

JPA

em.createQuery("select m from Member m where m.username = :username and m.age > :age")
             .setParameter("username", username)
             .setParameter("age", age)
             .getResultList();

Spring Data JPA

 @Query("select m from Member m where m.username = :username and m.age = :age")
    List<Member> findUser(@Param("username") String username, @Param("age") int age);

Querydsl

1) BooleanBuilder

    @Test
    public void dynamicQuery_BooleanBuilder(){

        //검색 조건
        String usernameParam = "member1";
        //Integer ageParam = 10;
        Integer ageParam = null;

        List<Member> result = searchMember1(usernameParam, ageParam);
        assertThat(result.size()).isEqualTo(1);

    }

    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();
    }
  • booleanBuilder에 초기값도 넣어줄 수 있다
  • 놀라운 것은 값이 있을 때만 비교를 하고 null이라면 그 조건을 뺀다는 것이다

2) WhereParam


    @Test
    public void dynamicQuery_WhereParam(){
        //검색 조건
        String usernameParam = "member1";
        //Integer ageParam = 10;
        Integer ageParam = null;

        List<Member> result = searchMember2(usernameParam, ageParam);
        assertThat(result.size()).isEqualTo(1);

    }

    private List<Member> searchMember2(String usernameCond, Integer ageCond) {

        return queryFactory.selectFrom(member)
                //.where(usernameEq(usernameCond), ageEq(ageCond)) 
                .where(allEq(usernameCond, ageCond)) 
                .fetch();
    }

  
 private BooleanExpression usernameEq(String usernameCond) {

        return usernameCond == null ? null : member.username.eq(usernameCond);

    }

    private BooleanExpression ageEq(Integer ageCond) {
        return ageCond != null ? member.age.eq(ageCond) : null;
    }




    private BooleanExpression allEq(String usernameCond, Integer ageCond){
        return usernameEq(usernameCond).and(ageEq(ageCond));
    }
  • 여러개의 조건을 조합해서 사용할 수 있다
  • ex ) 광고 상태 == isValid, 날짜가 == IN : isServicable 이런 식으로 만들 수 있다(깔끔 + 재활용)
  • null 뿐만 아니라 ""값이 올 때도 있는데 그럴 때는 hasText(str값)을 이용하면 된다

concat

Querydsl

//{username}_{age}
List<String> result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
                .from(member)
                .where(member.username.eq("member1"))
                .fetch();
  • age는 숫자이기 때문에 .stringValue()를 붙여 string type으로 변환함
  • 특히 enum type일 때 많이 사용

distinct

queryFactory.select(member.username).distinct()
	.from(member)
	.fetch();
  • 중복 제거

수업을 들으며 더 많은 내용을 들었지만 정말 많이 사용할 내용은
이정도일 것 같아서 이렇게 정리했다

확실히 책으로 공부하는것보다 빠르고 재밌게 공부할 수 있던 것 같다
(영한님 너무 깜찍하시다 재채기마저도 재밌게 하신다)
책도 예전에 사놔서 더 깊은 내용은 차차 공부해나가면 될 듯 하다

profile
나는야 누워있는 개발머신

0개의 댓글