[Spring] 연관관계 없이 Join 사용하여 DTO로 매핑시키기 - Spring Data JPA : Projection

kihongsi·2022년 3월 24일
1

spring

목록 보기
4/7

새로운 데이터를 불러와 기존 테이블의 데이터와 비교하여, 없던 것은 추가하고 있던 것은 수정하는 로직이 필요했다. 불러온 데이터를 새 테이블에 넣고 기존의 테이블과 비교하기 때문에 두 테이블의 컬럼이 완전히 동일하고, 두 테이블 간의 연관관계가 없어 결과값을 받는 DTO를 새로 만들어 받아오기로 했다.

구현 로직

기존에 Lecture 테이블이 있고, 새로 불러온 데이터를 저장할 NLecture 테이블을 생성했다.
두 테이블의 데이터는 20개의 컬럼 중 year, term(학기), subject_no(교과번호), class_div(분반) 컬럼만을 비교해주어 추가된 강의인지, 삭제된 강의인지, 또는 수정된 강의인지 필터링한다.
해당 조건을 비교해 full outer join 했을 때,

  • 기존 테이블 데이터가 null인 경우 : 새로 추가된 강의
  • 새 테이블 데이터가 null인 경우 : 삭제된 강의
  • 둘다 not null인 경우 : 정보 업데이트할 강의
    이렇게 분류해줄 것이다.

연관관계 없는 테이블 full outer join하기

union을 사용하여 full outer join의 결과를 불러올 수 있다.

select * from lecture
left outer join nlecture
on lecture.subject_no=nlecture.subject_no
and lecture.class_div=nlecture.class_div
where lecture.year=[년도] and lecture.term=[학기]
union
select * from lecture
right outer join nlecture
and lecture.year=nlecture.year
and lecture.term=nlecture.term
on lecture.subject_no=nlecture.subject_no
and lecture.class_div=nlecture.class_div

left outer join의 결과와 right outer join의 결과를 union 키워드로 합쳐준다.

Spring Data JPA - Projection

@Query를 날린 결과를 해당 entity가 아닌 다른 객체로 매핑하기 위해서는 Spring Data JPA의 Projection 기능을 사용해야 한다. 이름대로 엔티티의 일부 attribute만 projection(투사, 사영)하여 가져오고 싶을 때 사용할 수 있다.
보통 엔티티의 전체 컬럼이 필요하지 않을 경우 사용한다.

Interface-based Projection

반환받을 객체 형식을 Interface로 작성하여 가져오고자 하는 필드의 getter 메소드를 선언해주는 것이다. 이 때, 필드명이 정확히 매칭되어야 한다.

public interface UserView {
    String getName();
    String getNickname();
}
public interface UserRepository extends JpaRepository<User, Long> {
    List<UserView> findUserByAge(Integer age);
}

이런 식으로 작성하면, 런타임 시점에 UserView 타입의 proxy 인스턴스가 생성되어 리턴되고, UserView에 작성한 getName(), getNickname() 메소드를 실제 User 오브젝트에 전달하게 된다.

Class-based Projection

public class UserDTO {
    String name;
    String nickname;
    
    public UserDTO(String name, String nickname){
    	this.name = name;
        this.nickname = nickname;
    }
}

클래스를 사용하여 DTO를 작성하는 경우도 있다. 이 경우 proxy와 nested projection이 적용되지 않는다.

이외에도 제네릭을 사용한 Dynamic Projection 방식도 존재한다.

두개 이상의 Entity로 구성된 DTO

나의 경우, join 쿼리를 날려 Lecture, NLecture 두 개의 엔티티로 받아오기 위해 Projection을 사용하였다.

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class LectureJoinDTO {
	private Lecture lecture;
	private NLecture nLecture;
}

처음엔 class 형식을 사용해 DTO 안에 Lecture 엔티티와 NLecture 엔티티가 들어있는 형태로 작성하였다.
하지만 이렇게 작성하고 쿼리를 날렸을 때, ConverterNotFoundException 오류가 발생하였다.

org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type [com.uostime.server.dto.LectureJoinDTO]
	at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:322) ~[spring-core-5.3.16.jar:5.3.16]
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:195) ~[spring-core-5.3.16.jar:5.3.16]
	at
    ...

이 오류는 추후에 해결해보고 수정하겠습니다..

그래서 인터페이스 형식으로 작성해보았다.

public interface LectureJoinDTO {
	Lecture getLecture();
	NLecture getNLecture();
}

돌려봤을 때 잘 작동하였다.

JPQL로 join 쿼리 작성하기

아쉽게도 JPQL에서는 union을 지원하지 않았다.
union과 right join을 사용하려면 네이티브 쿼리를 사용해야 된다고 한다.
그래서 그냥 left join을 각 테이블에서 한번씩 사용해 필터링 해보았다.

@Repository
public interface LectureRepository extends JpaRepository<Lecture, Long>, JpaSpecificationExecutor<Lecture> {

	@Query("select l as lecture, n as NLecture from Lecture l left outer join NLecture n " +
			"on l.year = n.year and l.term = n.term and l.subjectNo = n.subjectNo and l.classDiv = n.classDiv " +
			"where l.year = :year and l.term = :term and l.used = true")
	List<LectureJoinDTO> lectureLeftJoin(String year, String term);
}
  1. Lecture 테이블을 기준으로 NLecture 테이블과 left outer join을 실행하여 DTO 리스트로 받아온다.
@Repository
public interface NLectureRepository extends JpaRepository<NLecture, Long> {

	@Query("select l as lecture, n as NLecture from NLecture n left outer join Lecture l " +
			"on l.year = n.year and l.term = n.term and l.subjectNo = n.subjectNo and l.classDiv = n.classDiv " +
			"and l.used = true")
	List<LectureJoinDTO> lectureRightJoin();
    
}
  1. NLecture 테이블을 기준으로 Lecture 테이블과 left outer join을 실행하여 DTO 리스트로 받아온다.

  2. 1의 결과값에서 NLecture 객체 null 검사를 통해 삭제, 업데이트할 데이터를 분류하여 처리한다

  3. 2의 결과값에서 Lecture 객체 null 검사를 통해 새로 추가된 데이터를 찾아내 db에 저장한다.


내가 작성한 로직이 효율적인지는 잘 모르겠다.
일단 의도한 대로 동작하는 데에 의의를 두고 ...
native query를 사용해 full outer join 방식으로도 구현해보고 싶다.



참고 문헌

Spring Data JPA Projections
Why, When and How to Use DTO Projections with JPA and Hibernate
Spring Data JPA - Join unrelated entities by defining projections
Join Unrelated Entities and Map the Result to POJO with Spring Data JPA and Hibernate

0개의 댓글