Sprint Data JPA 와 Querydsl 이용해서 Custom Repository 만들기

DongHyun Kim·2023년 8월 20일
1

Custom Repository ?

Spring Data Jpa 가 제공하는 기본 쿼리 메서드들 이외에도 복잡한 쿼리는 우리가 직접 작성해야할 일이 많다. 그렇다면 Service 에선 어떤 Repository 를 싱글톤으로 생성해야 할까?

인터페이스는 다중 상속이 가능하다는 성질을 이용하자!

기존 JpaRepsitory 를 상속받는 Repository 에서 추가로 Custom Repository 를 상속받을 수 있도록 할 수 있다.

아래 그림을 보고 구조를 눈에 익히자

  • Custom Repository 약속

일종의 약속인데, 추가로 상속 받을 인터페이스를 구현하는 클래스는 _____Impl 이란 이름을 가진다.

이를 안 지킬 시 다음과 같은 오류를 맞닥뜨린다.

Caused by: java.lang.IllegalArgumentException: Failed to create query for method public abstract java.util.List hello.itemservice.repository.custom.ItemRepositoryCustom.findItemByCondition(hello.itemservice.repository.ItemSearchCond)! No property 'condition' found for type 'Item'!

또한 인터페이스와 구현 클래스는 _____Impl 부분을 제외하곤 똑같도록 만들어줘야한다.

뒤에 Impl 대신 다른 것으로 붙이려면

@SpringBootApplication
@EnableJpaRepositories(repositoryImplementationPostfix = "Default")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@EnableJpaRepositories(repositoryImplementationPostfix=”원하는 접미사”) 로 설정하면 된다

CustomRepository 적용해보기

public interface JpaItemRepository extends MyItemRepository, JpaRepository<Item, Long> {

    Optional<Item> findById(Long id);

}

JpaItemRepository 는 JpaRepository 이외에 커스텀으로 만든MyItemRepository 또한 상속 받을 것이다

public interface MyItemRepository {

    List<Item> findAll(ItemSearchCond cond);

    void update(Long itemId, ItemUpdateDto updateParam);
}

Custom Repository 의 인터페이스, 구현해야 할 명세서 역할

@Transactional
public class MyItemRepositoryImpl implements MyItemRepository {

    private final EntityManager em;
    private final JPAQueryFactory query; // Querydsl 을 위한 변수

    public MyItemRepositoryImpl(EntityManager em){
        this.em = em;
        query = new JPAQueryFactory(em);
    }

    public BooleanExpression likeName(String itemName){

        if(StringUtils.hasText(itemName)){
            return QItem.item.itemName.contains(itemName);
        }
        return null;
    }

    public BooleanExpression maxPrice(Integer maxPrice){

        if(maxPrice != null){
            return QItem.item.price.loe(maxPrice);
        }
        return null;
    }

    @Override
    public List<Item> findAll(ItemSearchCond cond) {
            String itemName = cond.getItemName();
            Integer maxPrice = cond.getMaxPrice();

            QItem item = QItem.item;

            List<Item> items = query
                    .select(item)
                    .from(item)
                    .where(likeName(itemName), maxPrice(maxPrice))
                    .fetch();

            return items;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item findItem = em.find(Item.class, itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }
}

Custom Repository 를 구현하는 클래스. 반드시 인터페이스와 _____Impl 을 제외하고는 이름을 맞춰주자. (안 맞춰줘서 Querydsl 빈 생성 오류로 꽤나 고생했다)

Custom Basic Repository 만들기

Custom Basic Repository 는 CrudRepository 메서드를 오버라이드 하는 것이다.

나만의 save() 메서드를 커스텀해보면서 익숙해지자

public interface MyBasicRepository<T> {
    <S extends T> S save (S entity);
}

메서드를 정의할 인터페이스

@RequiredArgsConstructor
@Transactional
public class MyBasicRepositoryImpl<T> implements MyBasicRepository<T>{

    private final EntityManager entityManager;

    @Override
    public <S extends T> S save(S entity) {
        System.out.println("============== Customized Save ==============");
        entityManager.persist(entity);
        entityManager.flush();
        return entity;
    }
}

인터페이스에 정의한 메서드를 구현할 클래스

public interface JpaItemRepository extends MyItemRepository, MyBasicRepository<Item>, JpaRepository<Item, Long> {

    Optional<Item> findById(Long id);

}

주의할 점 ⚠️
인터페이스를 상속할 때 제네릭 타입을 구체적으로 명시해줘야 한다

인터페이스를 기존 JpaRepository<Item, Long>과 같이 상속하기 때문에 Custom Basic Repository 도 제네릭 타입을 구체적으로 명시해줘야한다.

아니면 이런 오류 발생

`save(S)' in 'hello.itemservice.repository.custom.MyBasicRepository' clashes with 'save(S)' in 'org.springframework.data.repository.CrudRepository'; both methods have same erasure, yet neither overrides the other

==== customized save ==== 가 출력되는 것 확인 가능!

깊게 공부하려다 뻘짓으로 된 여담

⚠️ CRUD Repository 쿼리 메서드인 findById 도 오버라이딩 할려 시도했다.
하지만 CRUD repository 와 Custom Repository 가 동일한 이름의 메서드지만 다른 반환, 매개변수 타입을 가져기 때문에 발생한 자바 컴파일 오류인 “ambiguous method” 도 있었고,
이 모호성을 피하기 위해 CRUD repository 의 findById 와 동일한 매개변수로 만들도록 제네릭 타입을 사용, Spring Data JPA repo 에 findById 생성했지만, 이를 구현하는 클래스에서 데이터 접근 기술 들 중 매개변수로 Class<T> 를 받는 메소드들이 많아서 포기!
(ex. entitymanager.find(Class<T> entityClass, Object primaryKey) )

중간에 포기한 이유는, 기존 CRUD repository의 쿼리 메서드를 재선언 하는 것은 좋은 방법이 아니기 때문이다. 개발을 편하게 하기 위해 쿼리 메서드가 생긴 것인데 오버라이드 하지 말고, 만약 정말 커스텀한 findById 기능이 필요하다면 메서드명을 myFindById 이렇게 바꾼 것을 사용하자.

출처 - Spring Data JPA - Reference Documentation

참고 - Spring Data JPA Tutorial: Adding Custom Methods to All Repositories

profile
do programming yourself

0개의 댓글