728x90

이번 포스팅에서는 조인에 대해 알아볼 것이다.

조인

조인은 innerJoin(join), leftJoin, rightJoin, fullJoin을 사용할 수 있고 추가로 JPQL의 on과 성능 최적화를 위한 fetchJoin을 사용할 수 있다.

jpaQueryFactory.selectFrom(item).join(조인할 쿼리클래스).fetch();

연관관계가 있으면 그냥 join만 사용해도 되지만 지금은 on이 추가되어 on() 절로 연관관계 없이 조인도 가능하다.

jpaQueryFactory.selectFrom(item).join(chair).on(chair.name.eq(item.name)).fetch();


from절에 여러 조건을 사용해서 세타조인도 가능하다.

서브 쿼리

서브 쿼리는 예전 버전에서는 JPASubQuery를 사용했지만 업데이트가 되어 현재 버전에서는 JPAExpressions 를 사용하여 서브쿼리를 작성한다.

@Test
@DisplayName("subQuery 테스트")
void subQueryTest() {
    jpaQueryFactory.selectFrom(item)
                   .where(item.name.eq(String.valueOf(JPAExpressions.selectFrom(chair).where(chair.name.eq("item3")))))
                   .fetch();
}

전 직장에서 근무하였을때 서브쿼리로 너무 많은것들을 처리했던 레거시 쿼리가 상당히 많아서
서브쿼리를 사용할때는 항상 생각해보고 쿼리를 짜야 좋을것 같다.
이게 다른 조회를 여러번 하는것이나 Join을 사용할때 보다 느린것 같다. 자세하게 속도를 비교해보면서 포스팅을 한번 했었어야 했는데 이게 안되서 개인적으로는 아쉬운 부분이다.😅

프로젝션

프로젝션 대상이 하나라면 해당하는 타입으로 결과를 받아야 한다.

List<String> list = jpaQueryFactory.select(item.name).from(item).orderBy(item.name.desc()).fetch();

그렇지만 대상이 여러개라면?
반환값이 Tuple인 객체를 반환하기 때문에 이렇게 사용해야 한다.

List<Tuple> list = jpaQueryFactory.select(item.name, item.price).from(item).orderBy(item.name.desc()).fetch();

가만보니까 이렇게 특정 칼럼만 받아서 쓰는 서비스 로직이 따로 있을거란 생각이 든다.
그때 그냥 DTO를 써주면 어떨까?

빈 생성

  • 프로퍼티 접근(Setter)
  • 필드 접근
  • 생성자 사용
    객체를 생성하는 방법 3가지 이다. 이것을 통해 그리고 Projections를 사용하여 dto객체를 생성해주면 될듯 하다.

주의❗️ 빈 생성자를 무조건적으로 넣어줘야 한다.

기본값이 빈 생성자라서 명시적으로 넣어주지 않았는데
다음과 같은 에러가 발생한다. 😱


protected 생성자 넣었을때 또 protected 에러를 발생시킨다.

그래서 lombok의 @NoArgsConstructor를 넣어주었더니 정상 실행이 된다.
public 접근제어자여야 한다.

@Test
@DisplayName("projection dto")
void dtoTest() {
        List<ItemDto> result = jpaQueryFactory.select(
        Projections.bean(ItemDto.class,item.name.as("name"), item.price))
        .from(item).fetch();
}

프로퍼티 접근 방식인데 여기선 Projections.bean을 사용한다. 이것이 setter 메소드를 사용해서 값을 채우는 방식이다.

필드 직접 접근 방식은 Projections.fields()를 사용하면 된다.

생성자 접근 방식은 Projections.constructor()를 사용하는데 지정한 프로젝션과 파라미터 순서가 같은 생성자여야 잘 동작한다.

수정, 삭제 배치 쿼리

이부분은 그냥 영속성 컨텍스트에서 삭제, 수정 해주는것이 더 편리할것 같아서 따로 정리하지는 않고 읽는 단계로 넘어가겠다.

동적 쿼리

여기가 내가 많이 생각했고 어떻게 해야 조건문으로 쿼리를 처리할지에 대한 고민을 했던게 이 부분인것 같다.

@Test
@DisplayName("동적 쿼리")
void 동적쿼리_Test() {
    SearchParam param = new SearchParam();
    param.setName(null);
    param.setPrice(200);

    BooleanBuilder booleanBuilder = new BooleanBuilder();
    if (!ObjectUtils.isEmpty(param.getName())) {
        System.out.println(param.getName());
        booleanBuilder.and(item.name.eq(param.getName()));
    }

    if (!ObjectUtils.isEmpty(param.getPrice())) {
        System.out.println(param.getPrice());
        booleanBuilder.and(item.price.eq(param.getPrice()));
    }

    List<Item> result = jpaQueryFactory.selectFrom(item).where(booleanBuilder).fetch();
}

BooleanBuilder를 사용하여 조건에따른 조건을 할당해서 조회해줄수가 있다.
이것을 할줄 몰라서 Mybatis를 고집했던 이유도 없지않아 있는것 같다.

JPQL이 기본인 CRUD보다 훨씬 중요하고 이것을 알아야 원하는대로 select 쿼리를 날려줄 수가 있다.
그러면서 동시에 이 QueryDSL을 쿼리로 만드는게 복잡하고 어렵다고 생각해서 mybatis에서 또는 그냥 @Query에서 못벗어난? 것 같다. 😭

여기에 리팩토링을 추가한다면 조건문 하나당 로직을 메서드로 분리하여 수행해주면 편할것이다.

728x90

'JPA' 카테고리의 다른 글

[JPA] findAll, findById 차이  (0) 2022.08.06
[JPA] 객체 지향 쿼리 언어 - Native SQL  (0) 2022.08.05
[JPA] 객체 지향 쿼리 언어 - Querydsl  (0) 2022.08.05
[JPA] 객체 지향 쿼리 언어 - 3  (0) 2022.08.05
728x90

QueryDSL

Criteria의 단점 너무 복잡하고 어렵다는 것 그래서 JPQL이 어떻게 생성되는지 파악이 어렵다.
그래서 나온게 이 QueryDSL이다.. 코드로 작성하는데 간결하고 알아보기 쉽다.

QueryDSL은 오픈소스 프로젝트이다. 단순 CRUD보다는 이름에 걸맞게 데이터를 조회 그러니까 통계형 쿼리를 짤때 적합하지 않을까 생각한다.

QueryDSL Setting

build.gradle

buildscript {
    ext {
        ...
        querydslVersion = '1.0.10'
    }

    dependencies {
        ....
        classpath "gradle.plugin.com.ewerk.gradle.plugins:querydsl-plugin:$querydslVersion"
    }
}

subprojects {
    apply plugin: 'com.ewerk.gradle.plugins.querydsl'

    ext {
        querydslDir = "$buildDir/generated/querydsl"
    }

    dependencies {
        ...
        implementation 'com.querydsl:querydsl-jpa'
    }

    querydsl {
        jpa = true
        querydslSourcesDir = querydslDir
    }

    sourceSets {
        main.java.srcDir querydslDir
    }

    configurations {
        querydsl.extendsFrom compileClasspath
    }

    compileQuerydsl {
        options.annotationProcessorPath = configurations.querydsl
    }
}

이렇게 설정을 해주었는데 중요한 부분은 subproject.ext.querydslDir 부분이다.
$buildDir이 뜻하는 것은 우리의 스터디는 일단 모듈을 나누어서 한사람당 모듈을 사용하고 있다.
그래서 의존성을 구분해놓았는데 여기서의 $buildDir은 모듈의 빌드된 폴더
build/를 의미하며 build/generated/querydsl 폴더에 Entity클래스 앞에 Q가 붙은 클래스가 빌드되어 있다.
이것으로 QueryDSL을 세팅해주는 것이다.

QuerydslConfig.java

@Configuration
public class QuerydslConfig {
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

전에 말했듯 JavaEE환경에서는 @PersistenceContext를 활성화하면 알아서 주입받는다.
JPAQueryFactory 가 QueryDSL을 사용하기 위해서 구현해야 하는 것이다.

이렇게 설정해주면 QueryDSL을 사용할 수가 있다.

이제 테스트 코드를 작성해 보자.

@DataJpaTest
public class QuerydslTest {
    @PersistenceUnit
    EntityManagerFactory emf;
    EntityManager em;
    EntityTransaction tx;
    JPAQueryFactory jpaQueryFactory;

    private Member member;

    @BeforeEach
    void setUp() {
        em = emf.createEntityManager();
        tx = em.getTransaction();
        jpaQueryFactory = new JPAQueryFactory(em);
        tx.begin();
        Address address = Address.builder()
                .city("city")
                .street("street")
                .zipcode("zipcode")
                .build();

        Address comAddress = Address.builder()
                .city("cocity")
                .street("costreet")
                .zipcode("cozipcode")
                .build();

        Period period = Period.of("20210714", "20210714");

        member = Member.builder()
                .name("kim")
                .age(26)
                .period(period)
                .homeAddress(address)
                .companyAddress(comAddress)
                .build();

        em.persist(member);
        em.flush();
    }

    @Test
    @DisplayName("QueryDSL 시작")
    void querydslTest() {
        QMember qMember = new QMember("m"); //생성된 별칭
        List<Member> members = jpaQueryFactory.selectFrom(qMember)
                .where(qMember.name.eq("kim")).orderBy(qMember.id.desc()).fetch();

        assertThat(members.get(0).getName()).isEqualTo("kim");
        assertThat(members.get(0).getAge()).isEqualTo(26);
    }
}

책에서는 orderBy(qMember.id.desc()).list(qMember); 로 사용했는데
버전이 바뀌면서 list 메소드는 없어졌다고 한다.
그래서 공식문서를 찾아보니까

QueryDSL로 select 조회 쿼리를 만들었을때 로그이다.

이상한점 이라고 느낄수 있다면 어? 하고 코드상에는 jpaQueryFactory.from으로 시작하기 때문에 이것이 어떤 CRUD인지 모를 수 있다.fetch()를 해주면 반환이 List인데 그래도 타입은 캐스팅을 해주어야 한다.
자동으로 뭔가 Q클래스를 맞춰줄줄 알았다.

🤣

ㅋㅋㅋㅋㅋㅋㅋ 아니었다..... 그냥 기본값이 select가 아니라 list(qMember) 를 해주던 버전에서는 이 list 메소드가 엔티티 타입을 맞춰서 List에 넣어주었는데 지금은 selectFrom(qMember)를 해주면 이것이 List 타입을 맞춰준다.
이렇게 또 하나 깨달음을 얻는다 👍

페이징 정렬

@Test
void 페이징_테스트() {
    QItem item = QItem.item;
    List<Item> result = jpaQueryFactory.selectFrom(item).where(item.price.lt(500))
                                           .orderBy(item.price.desc(), item.name.desc())
                                           .offset(1).limit(3).fetch();

    result.forEach(r -> System.out.println(r.toString()));
}

여기서 where 조건에 lt는 부등호로 < 이고, gt는 >이다. orderBy절에서는 쿼리 타입인(Q)에서 asc(), desc()를 지원해준다. 페이징은 offsetlimit 을 조합해서 사용하면 된다.
이렇게해서 얻은 결과이다.

전체 데이터 수를 알고 싶을때는

listResults()를 사용한다.

이것 역시 바뀌었다.
SearchResults<T> 가 아니라 버전이 바뀌면서 QueryResults<T>로 변경되었다.

listResults() 가 아니라 fetchResults()로 바뀌게 되었다.

@Test
@DisplayName("조회 결과 테스트")
void listResultsTest() {
    QueryResults<Item> result = jpaQueryFactory.selectFrom(item).where(item.price.lt(500))
                .orderBy(item.price.desc(), item.name.desc())
                .offset(1).limit(3).fetchResults();

    long total = result.getTotal(); //500보다 작은수 총 count
    long limit = result.getLimit(); //limit 3
    long offset = result.getOffset(); // offset 1
    List<Item> results = result.getResults();

    assertThat(total).isEqualTo(4L);
    assertThat(limit).isEqualTo(3L);
    assertThat(offset).isEqualTo(1L);

    results.forEach(r -> System.out.println(r.toString()));
}

getTotal은 오류의 여지가 있어보인다. 왜냐면 저 QueryDSL 전체를 시켰을때의 count가 아니라 count가 먼저 실행되기 때문에 where() 조건까지 수행한 count가 나온다.

결과의 조회는 QueryResultsgetResults()를 사용하여 페이징 정렬할때처럼의 결과를 얻을수가 있다.

그룹

그룹은 groupBy를 사용하고 그 다음에 조건을 해주려면 having을 사용하면 된다.

List<Item> results = jpaQueryFactory.selectFrom(item)
                                    .groupBy(item.price)
                                    .having(item.price.lt(500))
                                    .fetch();

728x90

+ Recent posts