querydsl은 JPA 쿼리를 동적 코드로써 작성할 수 있도록 도와주는 ORM 프레임워크입니다.
JPA에서 기본적으로 JPQL을 사용하여 레포지토리를 정의할 수 있습니다.
@Repository
public class ProductRepositoryImpl implements ProductRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<Product> findByName(String name) {
String jpql = "SELECT p FROM Product p WHERE p.name = :name";
return entityManager.createQuery(jpql, Product.class)
.setParameter("name", name)
.getResultList();
}
}
이 경우 SQL 쿼리를 큰 수정 없이 JPA에 적용할 수 있다는 장점이 있지만 오타 및 복잡한 조건의 적용 등으로 인해 유지보수성이 떨어지게 됩니다.
ORM을 좀더 객체 지향적으로 이용하기 위해서 우리는 동적 쿼리 생성을 할 수 있게 querydsl을 사용할 수 있습니다.
먼저 JpaRepository를 상속받아 레포지토리를 선언합니다.
public interface ResourceRepository extends JpaRepository<ResourceEntity, Integer> {
}
단 위 형태로 선언하게 되면 JpaRepository가 제공하는 기본 레포지토리만 사용할 수 있습니다. (findById 등)
우리는 개발을 진행하면서 여러 커스텀한 레포지토리의 정의가 필요하므로 커스텀한 레포지토리를 정의해 줄 수 있어야 합니다.
public interface ResourceRepository extends JpaRepository<ResourceEntity, Integer>, ResourceRepositoryDsl {
}
public interface ResourceRepositoryDsl {
// 리소스 조회
Optional<ResourceEntity> findByUUID(String uuid);
// 리소스 조회
Optional<ResourceDetailDTO> findDetail(int resourceId);
// 리소스 목록 조회
Page<ResourceListDTO> findAllResources(Pageable pageable);
// 리소스 목록 조회
List<ResourceListDTO> findAllResources();
}
위 코드 처럼 queydsl용 레포지토리 인터페이스를 정의하고 기존 레포지토리에서 상속받도록 합니다.
이후 인터페이스에 정의한 레포지토리 메서드들을 구현해 줍니다.
@Repository
public class ResourceRepositoryImpl extends QuerydslRepositorySupport implements ResourceRepositoryDsl {
private final JPAQueryFactory queryFactory;
public ResourceRepositoryImpl(JPAQueryFactory queryFactory) {
super(ResourceEntity.class);
this.queryFactory = queryFactory;
}
/**
* 리소스 조회
* @param uuid UUID
* @return 리소스
*/
@Override
public Optional<ResourceEntity> findByUUID(String uuid) {
BooleanBuilder conditions = new BooleanBuilder();
// 조회 대상 설정
JPAQuery<ResourceEntity> query = queryFactory.select(resourceEntity);
query.from(resourceEntity);
// 조건 설정
conditions.and(resourceEntity.uuid.eq(uuid));
query.where(conditions);
return Optional.ofNullable(query.fetchOne());
}
/**
* 리소스 조회
* @param resourceId 리소스 ID
* @return 리소스
*/
@Override
public Optional<ResourceDetailDTO> findDetail(int resourceId) {
BooleanBuilder conditions = new BooleanBuilder();
// 조회 대상 설정
JPAQuery<ResourceDetailDTO> query = queryFactory.select(Projections.fields(ResourceDetailDTO.class,
resourceEntity.resourceName
));
// 대상 설정
query.from(resourceEntity);
// 조건 설정
conditions.and(resourceEntity.id.eq(resourceId));
query.where(conditions);
return Optional.ofNullable(query.fetchOne());
}
/**
* 리소스 목록 조회
* @return 리소스 목록
*/
@Override
public List<ResourceListDTO> findAllResources() {
// 조회 대상 설정
JPAQuery<ResourceListDTO> query = queryFactory.select(Projections.fields(ResourceListDTO.class,
resourceEntity.id.as("resourceId"),
resourceEntity.resourceName,
resourceEntity.createdDate
));
query.from(resourceEntity);
return query.fetch();
}
/**
* 리소스 목록 조회
* @param pageable 페이지네이션 정보
* @return 리소스 페이지네이션
*/
@Override
public Page<ResourceListDTO> findAllResources(Pageable pageable) {
// 조회 대상 설정
JPAQuery<ResourceListDTO> query = queryFactory.select(Projections.fields(ResourceListDTO.class,
resourceEntity.id.as("resourceId"),
resourceEntity.resourceName,
resourceEntity.createdDate
));
query.from(resourceEntity);
// 결과 조회
List<ResourceListDTO> results = query.fetch();
// 전체 레코드 수 조회
JPAQuery<Integer> countQuery = queryFactory.select(resourceEntity.id);
countQuery.from(resourceEntity);
int count = query.fetch().size();
return new PageImpl(
results,
pageable,
count
);
}
}
메서드 구현까지 완료 되었다면 아래 형태의 구조로 레포지토리가 구성되게 됩니다.
여기서 우리는 Jpa 레포지토리인 ResourceRepository를 DI해서 사용할 수 있습니다.
우리가 레포지토리를 구현한 ResourceRepositoryDsl을 DI하여 사용할 경우 Jpa의 기본 제공 메서드들(findById 등)을 사용할 수 없으니 기존 레포지토리를 사용하는게 좋습니다.
/**
* 리소스 조회
* @param resourceId 번호
*/
public ResourceDetailDTO readResource(int resourceId) {
// 리소스 조회
ResourceDetailDTO resourceDetail = resourceRepository.findDetail(resourceId)
.orElseThrow(NotFoundException::new);
return resourceDetail;
}
하지만 querydsl은 서브 쿼리의 limit 옵션이 적용되지 않으며, union 또한 지원되지 않는 단점이 있습니다.
이 경우는 레포지토리 메서드를 분리해서 로직을 구현해야 할 것 같습니다.
처음 사용할 경우 엔티티 도메인의 관계 설정과 OneToMany 형태의 엔티티의 조건을 정의하는데 어려움을 겪게 될 수 있습니다.
엔티티의 관계 설정에 대한 자세한 내용은 이 포스트를 참고해 주세요
'개발 > java' 카테고리의 다른 글
Spring Data JPA란? (0) | 2023.05.21 |
---|---|
Spring Data JPA 관계 매핑하기 (0) | 2023.05.20 |
JPA 데이터 소스 여러개 적용하기 (0) | 2023.05.18 |
Github Action으로 Spring Boot Docker 이미지 배포하기 (0) | 2023.05.03 |
actuator사용시 Swagger 3.0과 충돌하는 현상 해결법 (0) | 2023.05.03 |