Develop/spring-data

Spring Data JPA 기반 Querydsl 사용해보자. ( Entity 관계 매핑 X편, 테스트 코드 포함 )

에디개발자 2020. 11. 8. 16:32
반응형

이전글 Querydsl 사용해보자. ( 설정편 )에서 설정하는 방법을 알아보았습니다. 

Entity 관계 매핑되있는 경우는Querydsl 사용해보자. ( Entity 관계 매핑 편, 테스트 코드 포함 )참조해주세요.

이번 글에서는 Querydsl의 버전업으로 Entity 관계 매핑되어있지 않아도 사용가능해졌기 때문에 Querydsl 사용편에 대해서 알아봅시다.

모든 소스는 github에 올려두었습니다. 

 

Entity

package com.example.querydsl.staff.entity;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Staff {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private Integer age;

    @Column(name = "store_id")
    private Long storeId;

    @Builder
    public Staff(Long id, String name, Integer age, Long storeId) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.storeId = storeId;
    }
}
package com.example.querydsl.store.entity;


import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Store {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String address;

    @Builder
    public Store(Long id, String name, String address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }
}

 

Repository

JPA Repository와 동일하게 사용합니다.

package com.example.querydsl.store.repository;

import com.example.querydsl.store.entity.Store;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StoreRepository extends JpaRepository<Store, Long> {
    Store findByName(String name);
}

 

package com.example.querydsl.staff.repository;


import com.example.querydsl.staff.entity.Staff;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StaffRepository extends JpaRepository<Staff, Long> {
}

 

RepositorySupport

Querydsl을 이용하여 실제 조회할 쿼리를 작성하는 클래스입니다.

QuerydslRepositorySupport 클래스를 상속받아서 사용합니다.

 

  • 밑의 클래스를 작성하시면 에러가 발생할 것입니다. Querydsl plugin을 실행시켜야 합니다.
  • 실행시키는 방법은 Querydsl plugin 사용방법을 참조해주세요.
package com.example.querydsl.store.support;


import com.example.querydsl.staff.entity.QStaff;
import com.example.querydsl.staff.entity.Staff;
import com.example.querydsl.store.entity.QStore;
import com.example.querydsl.store.entity.Store;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.stereotype.Repository;

import java.util.List;

import static com.example.querydsl.staff.entity.QStaff.*;
import static com.example.querydsl.store.entity.QStore.*;

@Repository
public class StoreRepositorySupport extends QuerydslRepositorySupport {
    private final JPAQueryFactory jpaQueryFactory;

    /**
     * Creates a new {@link QuerydslRepositorySupport} instance for the given domain type.
     *
     * @param domainClass must not be {@literal null}.
     */
    public StoreRepositorySupport(JPAQueryFactory jpaQueryFactory) {
        super(Store.class);
        this.jpaQueryFactory = jpaQueryFactory;
    }

    public List<Store> findByName(String name) {
        return jpaQueryFactory
                .selectFrom(store)
                .where(store.name.eq(name))
                .fetch();
    }

    public Store findOneByName(String name) {
        return jpaQueryFactory
                .selectFrom(store)
                .where(store.name.eq(name))
                .fetchOne();
    }

    /**
     * Entity 관계 매핑 되어 있는 경우
     * @param name
     * @return
     */
//    public List<Staff> findStaffsByName(String name) {
//        return jpaQueryFactory
//                .select(Projections.fields(Staff.class,
//                        staff.id
//                        , staff.age
//                        , staff.name
//                ))
//                .from(store)
//                .join(store.staff, staff)
//                .where(store.name.eq(name))
//                .fetch();
//    }

    /**
     * Entity 관계 매핑 되어 있지 않을 경우
     * @param name
     * @return
     */
    public List<Staff> findStaffsByName(String name) {
        return jpaQueryFactory
                .select(Projections.fields(StaffVo.class,
                        staff.id
                        , staff.age
                        , staff.name
                ))
                .from(store)
                .join(staff)
                    .on(store.id.eq(staff.storeId))
                .where(store.name.eq(name))
                .fetch();
    }
}
package com.example.querydsl.staff.support;

import com.example.querydsl.staff.entity.Staff;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.stereotype.Repository;

@Repository
public class StaffRepositorySupport extends QuerydslRepositorySupport {
    private final JPAQueryFactory jpaQueryFactory;

    /**
     * Creates a new {@link QuerydslRepositorySupport} instance for the given domain type.
     *
     * @param domainClass must not be {@literal null}.
     */
    public StaffRepositorySupport(JPAQueryFactory jpaQueryFactory) {
        super(Staff.class);
        this.jpaQueryFactory = jpaQueryFactory;
    }
}

RepositorySupport에서 QuerydslRepositorySupport를 상속받으면 생성자를 설정해야합니다.

 

import static com.example.querydsl.entity.QStaff.staff;
import static com.example.querydsl.entity.QStore.store;
  •  두줄이 중요합니다.
  • Querydsl 플러그인을 사용해서 Q클래스를 만들었고 어떻게 사용하는지 보여주는 부분입니다. static으로 선언하고 실제 로직에서는 store, staff 만 작성하여 사용하시면 됩니다. sql문과 사용하는 방식이 비슷하죠?

 

@param domainClass must not be {@literal null}.

주석에서 가이드한 것처럼 domain class를 설정해줘야합니다.

 

1) join을 걸때 Entity에서 관계 설정이 되어있는 기준으로 작성해주시면 됩니다.

 

제가 삽질한 내용 공유해드립니다.
Select fields 값 세팅 시 Staff Entity로 하시면 절대 안됩니다. 
Querydsl에서 조회된 값을 선언한 클래스에 접근하여 필드값을 세팅하려고 할 때 접근 권한이 없다고 나옵니다. 

Class com.querydsl.core.types.QBean can not access a member of class "entity" with modifiers "protected"

이유는 Entity는 protected 이기 때문입니다. 
자세한 내용은 여기를 참조해주세요.
.select(Projections.fields(StaffVo.class,))

 

 

테스트

Junit5를 사용하였습니다.

package com.example.querydsl.querydsl;

import com.example.querydsl.staff.entity.Staff;
import com.example.querydsl.staff.repository.StaffRepository;
import com.example.querydsl.store.entity.Store;
import com.example.querydsl.store.repository.StoreRepository;
import com.example.querydsl.store.support.StoreRepositorySupport;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@ActiveProfiles("local")
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class StoreRepositorySupportTest {

    @Autowired
    private StoreRepository storeRepository;

    @Autowired
    private StaffRepository staffRepository;

    @Autowired
    private StoreRepositorySupport storeRepositorySupport;

    @Test
    void findOneByNameTest() {
        //given
        final Long id = 5L;
        final String address = "주소5";
        final String name = "스토어5";

        Store store = Store.builder()
                .id(id)
                .address(address)
                .name(name)
                .build();

        storeRepository.save(store);

        //when
        Store resultByStore = storeRepositorySupport.findOneByName(name);


        //then
        Assertions.assertEquals(name, resultByStore.getName());
    }

    @Test
    void findStaffsByNameTest_Entity관계매핑안되어있는경우() {
        //given
        final Long staffId1 = 10L;
        final String staffName1 = "staffName4";
        final Integer age1 = 34;


        final Long staffId2 = 11L;
        final String staffName2 = "staffName5";
        final Integer age2 = 21;

        final Long id = 8L;
        final String address = "주소6";
        final String name = "스토어6";

        Staff staff1 = Staff.builder()
                .id(staffId1)
                .name(staffName1)
                .age(age1)
                .storeId(id)
                .build();

        Staff staff2 = Staff.builder()
                .id(staffId2)
                .name(staffName2)
                .age(age2)
                .storeId(id)
                .build();

        staffRepository.saveAll(Arrays.asList(staff1, staff2));

        Store store = Store.builder()
                .id(id)
                .address(address)
                .name(name)
                .build();

        storeRepository.save(store);

        //when
        List<Staff> staffs = storeRepositorySupport.findStaffsByName(name);

        //then
        assertThat(staffs.size()).isGreaterThan(0);
        assertThat(staffs.get(0).getName()).isEqualTo(staffName1);
        assertThat(staffs.get(1).getName()).isEqualTo(staffName2);
    }
}

하나씩 알아보겠습니다.

 

1 건 조회

@Test
void findOneByNameTest() {
    //given
    final Long id = 3L;
    final String address = "주소3";
    final String name = "스토어3";

    Store store = Store.builder()
            .id(id)
            .address(address)
            .name(name)
            .build();

    storeRepository.save(store);

    //when
    Store resultByStore = storeRepositorySupport.findOneByName(name);


    //then
    Assertions.assertEquals(name, resultByStore.getName());
}
  • given
    • jpa를 통해서 테스트에 필요한 데이터를 밀어넣습니다.
  • when
    • querydsl을 통해서 값을 조회합니다.
  • then
    • 데이터를 검증합니다.

 

Querydsl 사용해보자. ( 설정편 ) 에서 설정한 application.yaml이 있다면 로그에서 어떻게 쿼리를 생성해서 DB와 커넥션 맺어 사용하는지 볼 수 있습니다. 

밑에 두줄은 쿼리 실행 시 파라미터를 어떻게 세팅했는지 보여줍니다. 

 

데이터 Join 조회

@Test
void findStaffsByNameTest_Entity관계매핑안되어있는경우() {
    //given
    final Long staffId1 = 10L;
    final String staffName1 = "staffName4";
    final Integer age1 = 34;


    final Long staffId2 = 11L;
    final String staffName2 = "staffName5";
    final Integer age2 = 21;

    final Long id = 8L;
    final String address = "주소6";
    final String name = "스토어6";

    Staff staff1 = Staff.builder()
            .id(staffId1)
            .name(staffName1)
            .age(age1)
            .storeId(id)
            .build();

    Staff staff2 = Staff.builder()
            .id(staffId2)
            .name(staffName2)
            .age(age2)
            .storeId(id)
            .build();

    staffRepository.saveAll(Arrays.asList(staff1, staff2));

    Store store = Store.builder()
            .id(id)
            .address(address)
            .name(name)
            .build();

    storeRepository.save(store);

    //when
    List<Staff> staffs = storeRepositorySupport.findStaffsByName(name);

    //then
    assertThat(staffs.size()).isGreaterThan(0);
    assertThat(staffs.get(0).getName()).isEqualTo(staffName1);
    assertThat(staffs.get(1).getName()).isEqualTo(staffName2);
}
  • given
    • jpa를 통해서 테스트에 필요한 데이터를 밀어넣습니다.
  • when
    • querydsl을 통해서 값을 조회합니다.
    • 여기서 Entity 관계 매핑과의 다른점이 보입니다.
      • 관계 매핑일 경우 : join(store.staff, staff)
      • 관계 없을 경우 : join(staff).on(store.id.eq(staff.storeId)
  • then
    • 데이터를 검증합니다.

 

여기까지 Entity 관계 매핑이 되어있지 않을 경우 Querydsl 사용법에 대해서 알아보았습니다. 

다음은 Spring batch에서 QuerydslPagingItemReader 를 사용하는 방법을 알아보겠습니다.

 

 

 

 

 

반응형