개요

회사에서 쿼리작성도중 From절 Subquery가 필요한 상황이 있었는데, 서비스가 Spring Data JPA를 사용하고 있었는데, 아직 자료가 많이 없는 상황이라 도움이 되기를 바라며 쿼리를 작성한 경험을 공유하고자 글을 쓴다.

JPA에서의 From절 Subquery 사용가능 여부 확인

JPA에서 From절 Subquery로 많은 검색을 해봤지만, 쓸만한 내용이 없었고 검색 결과에 가장 많이 보이는 내용은
JPA의 한계라는 내용이었다. 많은 글에서 JPA는 From절 Subquery를 지원하지 않는다는 내용을 볼 수 있었고, 그 중 Hibernate 6.1 버전에서 Subquery에 관한 지원이 추가되었다는 사실을 알게 되었다.

이 사실을 통해 hibernate 문서를 뒤졌고, from절에서 subquery를 사용하는 예제를 찾을 수 있었다.

하지만, 현재 Spring Data JPA 버전에서 6.1버전 이상의 Hibernate를 사용했는지 여부를 확인해야 했기에, 프로젝트 내부에서 버전 정보를 확인했다.

보시다시피 Hibernate6.4.1 버전을 사용하고 있었기에, 충분히 JPA로 쿼리를 생성할 수 있겠다는 결론을 내렸고, From절 Subquery를 만들어 보기로 했다.

From 절 Subquery 작성

테스트를 위해 기존에 JPA 학습을 위해 생성한 레포지토리 내에서 몇가지를 수정하여 테스트를 시작했다.
파일 구성은 아래에 소개한다.

테스트 파일을 작성 할 수도 있겠지만, 있었던 controller를 수정하는 것이 더 빠를것 같아서 Controller를 아래처럼 수정했다.

package com.hyeonwoo.spring.jpa.springjpa;

import com.hyeonwoo.spring.jpa.springjpa.domain.Address;
import com.hyeonwoo.spring.jpa.springjpa.domain.Member;
import com.hyeonwoo.spring.jpa.springjpa.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.ArrayList;

@Controller
@RequiredArgsConstructor
public class HelloController {
    private final MemberService memberService;

    @GetMapping("test")
    public void test(Model model) {
        memberService.testSpringMember();
    }

    @GetMapping("insert")
    public void insert() {
        Member a = new Member(1L,"박현우", new Address("Seoul", "Main Street", "12345"));
        Member b = new Member(2L,"차은우", new Address("Seoul", "Main Street", "12345"));
        Member c = new Member(3L,"장원영", new Address("Seoul", "Main Street", "12345"));
        Member d = new Member(4L,"카리나", new Address("Seoul", "Main Street", "12345"));
        Member e = new Member(5L,"홍윤기", new Address("Seoul", "Main Street", "12345"));

        memberService.insertTestDataSet(a);
        memberService.insertTestDataSet(b);
        memberService.insertTestDataSet(c);
        memberService.insertTestDataSet(d);
        memberService.insertTestDataSet(e);
    }
}

아래는 Service 클래스이다.

Service 클래스에서는 Spring Data JPA를 통해 쿼리를 생성하는 방법과 Entity Manager를 직접 다루는 방법 두가지를 시도해보았다.

package com.hyeonwoo.spring.jpa.springjpa.service;


import com.hyeonwoo.spring.jpa.springjpa.domain.Address;
import com.hyeonwoo.spring.jpa.springjpa.domain.Member;
import com.hyeonwoo.spring.jpa.springjpa.repository.MemberRepository;
import com.hyeonwoo.spring.jpa.springjpa.repository.SpringMemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    private final SpringMemberRepository springMemberRepository;

    public void testSpringMember() {
        List<Member> test = springMemberRepository.findByInlineViewTest("박현우");
        log.info(test.toString());
        // EXCEPTION
    }

    public void testMember() {
        List<Member> test = memberRepository.findByInlineViewTest("박현우");
        log.info(test.toString());
        // EXCEPTION
    }

    @Transactional
    public void insertTestDataSet(Member a) {
        springMemberRepository.save(a);
    }
}

아래는 Spring Data JPA를 통해 서브쿼리를 사용해보는 Repository 예제 코드이다.

Select 조건에 new Member라는 생성자를 통해 데이터를 삽입했는데, 생성자를 사용하지 않을 시 오류가 발생하여 아래의 StackOverflow 포스팅을 참고했다.

https://stackoverflow.com/questions/67500203/failed-to-convert-from-type-java-lang-object-to-type-org-springframework-d

 

Failed to convert from type [java.lang.Object[]] to type [@org.springframework.data.jpa.repository.Query com.data.models.Users]

I am trying to limit my query to only select specific columns from a table which am going to use, but when I write a simple select query I end up with an error Resolved [org.springframework.core.c...

stackoverflow.com

package com.hyeonwoo.spring.jpa.springjpa.repository;

import com.hyeonwoo.spring.jpa.springjpa.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface SpringMemberRepository extends JpaRepository<Member, Long> {

    @Query("SELECT new Member(T.id as id,T.name as name,T.address as address) FROM (SELECT T2.id as id,T2.name as name,T2.address as address FROM Member T2 WHERE T2.id = 1) T")
    List<Member> findByInlineViewTest(@Param("name") String name);
}

EntityManager를 사용하여 쿼리를 생성한 예제이다.

package com.hyeonwoo.spring.jpa.springjpa.repository;

import com.hyeonwoo.spring.jpa.springjpa.domain.Member;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private final EntityManager em;

    public List<Member> findByInlineViewTest(String name) {
        return em.createQuery("SELECT T.id, T.name, T.address FROM ( SELECT T2.id as id,T2.name as name,T2.address as address FROM Member T2 WHERE T2.id = 1) T WHERE T.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();
    }
}

마지막으로 Entity 클래스이다. 특별한 내용은 없다.

package com.hyeonwoo.spring.jpa.springjpa.domain;

import jakarta.persistence.*;
import lombok.*;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter @Setter
@RequiredArgsConstructor
@AllArgsConstructor
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String name;

    @Embedded
    private Address address;

}

package com.hyeonwoo.spring.jpa.springjpa.domain;

import jakarta.persistence.Embeddable;
import lombok.Getter;

@Embeddable
@Getter
public class Address { // 값 타입은 변경 불가능하게 설계해야 한다.
    private String city;
    private String street;
    private String zipCode;
    protected Address(){} // 임베디드 타입은 자바 기본 생성자를 public 또는 protected로 설정해야 한다.


    // Setter를 사용하지 않고 생성자에서 값을 모두 초기화 해서 변경 불가능한 클래스로 제공
    public Address(String city, String street, String zipCode) {
        this.city = city;
        this.street = street;
        this.zipCode = zipCode;
    }
}

위처럼 파일 세팅 후 H2 데이터베이스에 데이터를 임시로 삽입하는 API를 호출했고, 이후 서브쿼리를 통해 데이터를 조회하는 부분에서 디버깅을 통해 데이터가 잘 들어오는지 확인할 수 있었다.

알게된 점

  • hibernate 6.1버전 이상에서는 JPA를 통해 From 절 Subquery SQL을 작성 할 수 있다.
  • Subquery 작성 시 alias를 작성하지 않으면 이름을 찾지 못하는 오류가 있는 것 같다. 예제에서도 alias가 필수적으로 붙어있는 이유로 보인다.
  • Entity 타입으로 데이터를 조회할 수 있었지만, class Projection을 통해서는 데이터를 수신하는데 실패하고 있다. 이 부분은 좀 더 확인해보고, 공유할 만한 내용이 있다면 공유할 생각이다.

 

https://in.relation.to/2022/06/14/orm-61-final/

 

Hibernate 6.1 Final - In Relation To

Hibernate ORM 6.1 was released last week with a few new features[1] and bugfixes. The new features include:

in.relation.to

 

https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html

 

Hibernate ORM 6.1.7.Final User Guide

Fetching, essentially, is the process of grabbing data from the database and making it available to the application. Tuning how an application does fetching is one of the biggest factors in determining how an application will perform. Fetching too much dat

docs.jboss.org

 

'Spring' 카테고리의 다른 글

Spring Boot에서 Runtime Logging Level 변경하기  (1) 2024.09.03
Spring Boot 서버 포트 변경하기  (1) 2023.10.03

+ Recent posts