쿼리DSL

김민지·2022년 10월 27일
0

JPA

목록 보기
16/27

쿼리DSL과 JPQL

  • JPQL 대산 Querydsl 을 이용하는 이유 중 하나가 type-safe (컴파일 시점에 알 수 있는) 쿼리를 날리기 위해서 사용한다.

이 말은 JPQL 에서 쿼리에서 오타가 발생해도 컴파일 시점에서 알기 힘들다. 오로지 런타임에서만 체크가 가능하다. 하지만 Querydsl 은 컴파일 시점에 오류를 잡아줄 수 있기 떄문에 좋다.

Querydsl 동작 하는 과정은 JPQL 을 거쳐서 SQL 로 변환되서 실행한다.

  • 어떻게 하라는거지? 별도로 어떻게 내?

조인

  • .join(member.team, team1)에 넣어주는 team1은 QTeam을 선언해서 넣어줘야한다

세타조인

  • from 에 member, team이렇게 넣는다

on, where의 차이

ON 절과 WHERE 절의 차이는 ON 절 같은 경우는 JOIN 할 데이터를 필터하기 위해서 사용하는 반면에 WHERE 절은 JOIN 을 하고나서 데이터를 필터하기 위해서 사용한다고 생각하면 된다. 즉 ON 절이 WHERE 절보다 먼저 실행이 되고 이는 LEFT_OUTER_JOIN 을 하면 뚜렷히 드러난다.

차이 실습

@Test
	@Transactional
	@DisplayName("예) 회원과 팀을 조인하면서, 팀 이름이 TeamA인 팀만 조인 하고 회원은 모두 조회한다.")
	void joinOnFiltering(){
		//given
		biSetting();
		QMember member = QMember.member;
		QTeam team = QTeam.team;
		//when
		List<Tuple> result = factory
				.select(member, team)
				.from(member)
				.leftJoin(member.team, team)
				.on(team.name.eq("team1"))
				.fetch();
		//then
		for (Tuple tuple : result) {
			System.out.print(tuple.get(member).getName());
			if(tuple.get(team)!=null) System.out.println("   "+tuple.get(team).getName());
			else System.out.println("  null");
		}
	}
  • on 사용
  • where

    left outer join을할때는 on과 where사용에 유의하자

페치조인

  • 패치조인은 SQL 에서 제공하는 기능은 아니다. JPA 에서 주로 성능 최적화를 위해서 사용하는 기능이다.
  • 연관된 엔티티나 컬렉션을 한번에 조회하는 기능

프록시가 초기화 되었는지 확인

emf.getPersistenceUnitUtil().isLoaded(m2));

서브쿼리

  • 서브 쿼리란 SELECT 문 안에 다시 SELECT 문이 기술된 형태의 쿼리로 안에 있는 결과를 밖에서 받아서 처리하는 구조다.
    단일 SELECT 문으로 조건식을 만들기가 복잡한 경우에 또는 완전히 다른 테이블에서 값을 조회해서 메인 쿼리로 이용하고자 할 때 사용한다.
    서브 쿼리는 from 절은 안되고 select 절이나 where 절에서만 가능하다. (원래는 select 절에도 안된다.)
    JPAExpressions 을 static import 해서 줄이는 것도 추천한다.

서브쿼리랑 메인쿼리에서 같은 별칭을 쓰면 안되는 이유

  • 별칭이 겹칠 경우 서브쿼리 내에서는 별칭 사용시 식별문제가 발생. 그래서 서브쿼리의 결과에 영향을 미침

Case 문

조건에 따라서 값을 지정해주는 CASE 문은 select, 조건절(where), orderBy 에서 사용이 가능하다.

상수

상수가 필요하다면 Expressions.constant() 를 사용하면 된다. 줄여서 쓰고 싶다면 static import 를 사용하자.

문자더하기

concat사용

Projection 과 결과 반환

Projection 은 엔터티를 그냥 그대로 가지고 오는게 아니라 필요한 필드만 가지고 오는 걸 말한다.
Querydsl 에서는 프로젝션 대상이 하나면 명확한 타입을 지정할 수 있지만 프로젝션 대상이 둘 이상이라면 Tuple 이나 DTO 로 조회해야 한다.

순수 jpa

@Test
void projectionWithJpa(){
    //given

    //when
    List<MemberDto> result = em.createQuery(
            "select new com.study.querydsl.dto.MemberDto(m.username, m.age)" +
                    "from Member m", MemberDto.class
            )
            .getResultList();
    //then
    for (MemberDto memberDto : result){
        System.out.println(memberDto.toString());
    }
}
  • 순수 JPA 에서 DTO 를 조회할 때는 new 키워드를 이용한 생성자를 통해서만 가능했다.
    그리고 package 이름을 모두 명시해야해서 좀 지저분함이 있었다.

쿼리DSL을 이용한 DTO생성

  • 프로퍼티로 접근하는 방식 (Setter 사용)
    ㄴ Projections.bean 사용
    ㄴ Projections.bean() 을 사용하면 기본 생성자와 setter 를 통해서 객체를 만들게 된다.
  • 필드 직접 접근
    ㄴ Projections.fields() 를 통해서 getter setter 필요없이 바로 필드로 직접 주입해서 사용한다.
    ㄴ private 로 선언해도 상관없다. 사실상 자바 리플렉션을 이용하면 private 상관없이 다 알수있다.
  • 생성자 사용
    ㄴ Projections.constructor() 사용

서브쿼리 & Projection

@Test
	void 서브쿼리_projection(){
		//given
		QMember member1 = new QMember("M");
		//when
		List<jpaStudy.ex.dto.MemberDto> result = factory.select(Projections.fields(jpaStudy.ex.dto.MemberDto.class, QMember.member.name,
				ExpressionUtils.as(
						JPAExpressions.select(member1.age.max()).from(member1), "age"
				)
				)).from(QMember.member).fetch();
		//then
		result.forEach(e -> {
			System.out.println(e.getName() + " " + e.getAge());
			Assertions.assertThat(e.getAge()).isEqualTo(113);
		});
	}

@QueryProjection

  • DTO 생성자에 @QueryProjection 이 붙이면 이후 빌드 툴을 이용해 compile 하면 Q타입의 클래스가 생성된다.
  • new QMemberDto(member.username, member.age) 와 같이 사용하면 된다

장단점

  • Projections.constructor() 와의 차이는 컴파일 오류를 못잡는다는 문제가 생긴다. 위 방식이 좀 더 안정성이 있다.
  • 컴파일 시에 타입 체크를 할 수 있다
  • 순서를 지켜야하는것은 변함없긴함
  • 다만 이 방식의 문제점은 Querydsl 에 대한 의존성을 가지게 된다는 점이다. 라이브러리를 바꾸게 된다면 고쳐야할 DTO 가 많아진다는 단점이 있다.
    https://cheese10yun.github.io/querydsl-projections/

동적쿼리

  • BooleanBuilder 사용
  • 실행시에 쿼리 문장이 만들어져 실행되는 쿼리문

쿼리DSL에서 동적쿼리를 만드는 방법은 다음 두가지 방법이 있다

  • BooleanBuilder
  • Where 다중 피라미터 사용 (추천)
    ㄴ조건조립을 통해 재사용성을 높임

벌크연산

  • 쿼리 한번으로 대량의 데이터를 수정하는 방식
  • 벌크 연산은 조심해야 되는게 있다. JPA 에는 영속성 컨택스트가 메모리에 올라와 있다. 하지만 벌크 연산은 DB 에 바로 반영하는거기 때문에 영속성 컨택스트의 상태와 DB 의 상태가 달라지게 된다.
  • 즉 벌크 연산을 한 후에 fetch() 로 데이터를 조회할려고 해도 영속성 컨택스트에 값이 있다면 변경된 값을 DB 에서 가지고 와도 1차 캐시에 있는 값을 전달해준다

    변경된 값을 가지고 오기 위해서는 em.flush() 와 em.clear() 를 통해서 영속성 컨택스트 값을 버리면 된다.

주의

select 는 다음과 같은 과정을 거친다
1. flush
2. 디비로 쿼리날림
3. 쿼리결과가 영속성컨텍스트에 있으면 버리고 없으면 저장 .
update는 업데이트된 객체를 가져오진 않는다.
그래서 1,2 과정만 거친다
쿼리dsl에서는 insert가 없어서 persist를 사용한다

데이터 삽입

JPAQueryFactory는 insert()를 가지고 있지 않다.
insert를 하려면 EntityManager를 사용하자! (SQLQueryFactory는 가능하다)

예제1

public List<MemberTeamDto> searchByWhere(MemberSearchCondition condition){
        return queryFactory.select(new QMemberTeamDto(
                QMember.member.id.as("memberId"),
                QMember.member.name.as("username"),
                QMember.member.age,//생성자 순서에 잘 맞춰야함
                QTeam.team.id.as("teamId"),
                QTeam.team.name.as("teamName")
        )).from(QMember.member).leftJoin(QMember.member.team, QTeam.team).where(teamNameEq(condition.getTeamName()),memberNameEq(condition.getUsername()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe())).fetch();
    }
  • where-> on으로바꾸면 error남
  • on이면 저상태에 따라 필터링하고 나서 join을함 근데 왜 오류가 생기는걸까?
    java.lang.NullPointerException: Cannot invoke "com.querydsl.core.types.Expression.accept(com.querydsl.core.types.Visitor, Object)" because "expr" is null
  • on인 경우고, 인자로 null이 들어가면 error가 난다 왜그러는걸까?
    select m from Member m leftjoin Team t on :name=t.name
    이런 문장이 있다고 해요. 근데 이게 name에 null이 들어가면 error가 떠요
    근데 where이면 에러가 안뜨고 on일 경우에만 에러가 떠요.. 이거 왜그러는걸까요?

네번 출력

 public List<Member> test(MemberSearchCondition condition){
        return queryFactory.select(QMember.member)
                .from(QMember.member, QTeam.team)
                .where(memberNameEq(condition.getUsername()))
                .fetch();
    }
 @Test
    @Transactional
        //TODO: 왜이거안붙이면 프록시초기화안됐다더?  그이유는 트랜잭션안에있어야 영속성컨텍스트의 관리를바당서그런건가"?
    //repository에 붙은 트랜잭션ㅇ이 끝나면서 프록시 객체인 team의 name을 읽으려해서 그런거임
    void 왜네번이나출력되지(){
        //given
        //when
        List<Member> list = repository.test(new MemberSearchCondition("mem1", null , null,null));
        //then
        list.forEach(e -> {
            System.out.println(e.getName() + " " + e.getTeam().getName() + " " + e.getId() + " " );
        });
    }
  1. Transcational을 안붙이면 프록시 초기화가 안되는 이유 : repository에 붙은 트랜잭션ㅇ이 끝나면서 프록시 객체인 team의 name을 읽으려해서 그런거임
  2. 네번이나 출력되는 이유: 로그보면 크로스조인을 사용하고 있음.
public List<Member> test(MemberSearchCondition condition){
        return queryFactory.selectFrom(QMember.member).join(QMember.member.team, QTeam.team).fetchJoin()
                .where(memberNameEq(condition.getUsername()))
                .fetch();
    }
  • 크로스 조인은 이런식으로 사용해야함
  • 주의 깊게 보셔야할 점은 별도의 join절이 선언되어 있지 않고 where문에서 바로 연관관계 (customer.shop.shopNo) 를 사용한다는 점인데요.
    이렇게 작성할 경우 Cross Join이 발생합니다.

출처
https://github.com/Youngerjesus/Querydsl/blob/master/docs/basic.md
https://jojoldu.tistory.com/533

profile
안녕하세요!

0개의 댓글