Develop/spring-data

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

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

이전 글에서 Querydsl 설정하는 방법에 대해서 알아보았습니다. 

이 글은 Entity 관계 매핑이 되어 있는 경우 Querydsl 사용법에 대해서 알아보겠습니다.

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



나를 닮았다고 한다...

Entity

package com.example.queyrdsl.entity;


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

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

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

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

    @OneToMany(mappedBy = "store")
    private List<Staff> staff = new ArrayList<>();

    @Builder
    public Store(Long id, String name, String address, List<Staff> staff) {
        this.id = id;
        this.name = name;
        this.address = address;
        this.staff = staff;
    }
}
package com.example.queyrdsl.entity;

import lombok.*;

import javax.persistence.*;

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

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

    private String name;
    private Integer age;

    @ManyToOne(targetEntity = Store.class, fetch = FetchType.LAZY)
    @JoinColumn(name = "store_id", foreignKey = @ForeignKey(name = "fk_staff_store_id"))
    private Store store;

    @Builder
    public Staff(Long id, String name, Integer age, Store store) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.store = store;
    }
}

Store와 Staff 는 1 : N 관계입니다.

두 Entity 모두 Access Level 이 public 입니다. 그 이유는 이글을 참조해주세요.

 

Store에서 mappedBy를 사용하였습니다. 

Staff에서 쓸데없는 리소스를 줄이기 위해 FetchType.LAZY로 선언하였습니다.

mappedBy 
내가 대장이다. 선언한 Entity는 다른 설정을 안해도 됩니다.
join 걸리는 Entity에서 설정(targetEntity, fetch)하면 됩니다.

 

Repository

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

package com.example.queyrdsl.store.repository;

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

public interface StoreRepository extends JpaRepository<Store, Long> {
    Store findByName(String name);
}
package com.example.queyrdsl.staff.repository;


import com.example.queyrdsl.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.queyrdsl.repository.support;


import com.example.queyrdsl.entity.Staff;
import com.example.queyrdsl.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.queyrdsl.entity.QStaff.staff;
import static com.example.queyrdsl.entity.QStore.store;

@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 Store findOneByName(String name) {
        return jpaQueryFactory
                .selectFrom(store)
                .where(store.name.eq(name))
                .fetchOne();
    }

    public List<StaffVo> findStaffsByName(String name) {
        return jpaQueryFactory
                .select(Projections.fields(StaffVo.class,
                        staff.id
                        , staff.age
                        , staff.name
                ))
                .from(store)
                .join(store.staff, staff)		// 1)
                .where(store.name.eq(name))
                .fetch();
    }
}
package com.example.queyrdsl.repository.support;

import com.example.queyrdsl.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.queyrdsl.entity.QStaff.staff;
import static com.example.queyrdsl.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.queyrdsl.querydsl;

import com.example.queyrdsl.entity.Staff;
import com.example.queyrdsl.entity.Store;
import com.example.queyrdsl.repository.StoreRepository;
import com.example.queyrdsl.repository.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 StoreRepositorySupport storeRepositorySupport;

    @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());
    }

    @Test
    void findStaffsByNameTest() {
        //given
        final Long staffId1 = 2L;
        final String staffName1 = "staffName1";
        final Integer age1 = 31;


        final Long staffId2 = 3L;
        final String staffName2 = "staffName2";
        final Integer age2 = 41;

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

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

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

        Store store = Store.builder()
                .id(id)
                .address(address)
                .name(name)
                .staff(Arrays.asList(staff1, staff2))
                .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 조회

void findStaffsByNameTest_Entity관계매핑되어있을경우() {
    //given
    final Long staffId1 = 2L;
    final String staffName1 = "staffName1";
    final Integer age1 = 31;


    final Long staffId2 = 3L;
    final String staffName2 = "staffName2";
    final Integer age2 = 41;

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

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

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

    Store store = Store.builder()
            .id(id)
            .address(address)
            .name(name)
            .staff(Arrays.asList(staff1, staff2))
            .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을 통해서 값을 조회합니다.
    • 여기서 위 1건 조회와 다른점은 join 부분입니다. findStaffsByName의 메소드를 보시면 join(store.staff, staff) 부분이 있습니다. 이렇게 설명하면 내부에서 @JOIN_COLUMN 기준으로 조인시켜줍니다. 
  • then
    • 데이터를 검증합니다.

 

 

여기까지 Entity 관계 매핑 있을 경우 Querydsl 사용방법에 대해서 알아봤습니다.

Querydsl 버전이 낮을 땐 Entity 관계 매핑이 되어있을 경우에만 사용 가능했습니다. 하지만 버전업되면서 Entity에 관계 매핑이 되어있지 않아도 사용가능해졌습니다. 

제 경험으로는 Querydsl을 사용한다면 Entity 관계 매핑을 제외하고 사용하는 것이 더 편리했습니다. 상황에 맞게 사용해주세요 :) 

 

다음은 관계매핑 없는 경우 Querydsl 사용방법에 대해서 정리해보겠습니다.

 

반응형