Hibernate가 N+1 선택을 위해 구조 함수 표현식을 사용하지 않도록 합니다.

14772 단어 jpahibernatesqljpql

동기


JPA/Hibernate의 기본 CRUD 작업은 매우 쉽지만, 모든 응용 프로그램은 조만간 특정 용례(예를 들어 투영)에 DTO 스타일의 결과 대상을 도입해야 한다.이것은 거래의 일부분으로 아무도 또는 지도 제작자가 당신을 도와 공부를 할 수 없습니다.
다행히도 JPA는 우리가 조회에서 기대하는 결과 대상을 정확하게 지정할 수 있는 방법을 제공했다.한 가지 방법은 조회의 선택 부분에서 JPQL과 이른바'구조 함수 표현식'을 사용하는 것이다.
select new com.example.NameDto(e.firstName, e.lastName) from Employee e
new 키워드와 완전히 한정된 클래스 이름을 주의하십시오.이것이 바로 네가 그것을 할 것이라고 생각하는 것이다.그것은 모든 결과행 NameDto 을 만드는 데 적당한 공공 구조 함수를 사용합니다.

문제


솔리드의 일부를 사용하여 DTO에 투영하는 것이 좋습니다. 하지만 전체 솔리드를 가져와 DTO에 포장하려면 어떤 일이 일어날까요?이 용례에서 우리는 실체를 선택하고 싶지만 실체와 함께 저장되지 않은 보조 정보를 선택해야 한다고 상상해 보세요.예를 들어, 만약 우리가'2000m 반경 내에서 내 근처에 있는 곳'을 선택하려고 한다고 가정하자.이것은 유일한 조건이다. 그리고 너희들은 실제 위치 실체의 결과집을 가지고 있다. 그렇지?만약 우리가 최종 사용자에게 이 곳들이 그의 위치에서 얼마나 멀었는지 보여주고 싶다면?왜 우리는 Place 실체를 적당한 DTO로 포장하지 않고 거리를 유지합니까? 예를 들어 DistanceResult?한번 해볼게요.
여기에는 Place 실체가 있습니다. 위치를 표시하는 위치 필드가 있습니다.

그리고 나서 우리는 DistanceResult라는 DTO를 만들었다. 이 DTO는 우리의 실체를 한데 포장하고 조회의 원점에서 얼마나 멀리 떨어져 있는지 설명하기만 하면 된다.

다음과 같은 간단한 JPQL 질의를 시작합니다.
select new com.example.DistanceResult(p, distance(:center, p.location))
  from Place p
  where dwithin(:center, p.location, :radiusMeters) = true
우리가 여기서 토론하는 지리적 유형과 기능을 잠시 소홀히 하다.이것은 사실상 hibernate-spatial의 일부분이다. 나는 postgis의 실례에서 그것을 사용했지만 이것은 여기서 중요하지 않다.
이것은 우리의 조회 로그가 이 조회에 제공한 정보입니다.
select place1_.id as col_0_0_, st_distance(?, place0_.location) as col_1_0_ from places place0_ inner join places place1_ on (place0_.id=place1_.id) where st_dwithin(?, place0_.location, ?)=true

select place0_.id as id1_0_0_, place0_.location as location2_0_0_, place0_.name as name3_0_0_ from places place0_ where place0_.id=?

select place0_.id as id1_0_0_, place0_.location as location2_0_0_, place0_.name as name3_0_0_ from places place0_ where place0_.id=?
우리는 우리의 단일 JPQL 조회가 대체적으로 그 SQL에 해당하는 조회를 생성하는 것을 똑똑히 볼 수 있다.가장 중요한 것은, 우리는 결과에서 모든 실체의 실례를 위해 하나를 선택할 수 있다는 것이다
설정 (여기 결과 크기는 2).이것은 무서운 N+1 문제로 보인다.

솔루션 1: 결과 변환기


한편, Hibernate 프로젝트의 어떤 사람은 왜 구조 함수 표현식을 사용하면 완전한 실체를 얻을 수 없고 직접 필요한 부분만 얻을 수 있는지 추리가 있다.다른 한편, 주어진 용례에서 N+1은 우리가 원하는 것이 아니다.유감스럽게도hibernate가 전체 실체를 얻을 수 있는 명확한 방법이 없습니다.예를 들어, fetch 키워드를 사용하는 자체 연결은 작동하지 않습니다.이 경우 fetch all properties 절도 생성된 SQL에 영향을 주지 않습니다.
다행히도 우리가 원하는 DTO 실례화를 실현할 수 있는 다른 방법이 있다.이것은 휴면에 특정한 것으로 ResultTransformer 라고 부른다.모든 Hibernate 조회를 지정하면 이 인터페이스의 실례를 추가하고 그 결과를 처리할 수 있습니다.

Spring 데이터 JPA가 있는 결과 변환기


대부분의 JPA 및/또는hibernate 사용자가 템플릿을 피하기 위해 Spring 데이터 JPA를 사용할 수 있다고 믿습니다.다음은 이 경우 어떻게 사용하는가ResultTransformer입니다.
우선, 우리는 일반적인 메모리 라이브러리 인터페이스가 있는데, 어떤 화려한 것도 없다. 이것은 추가 인터페이스를 확장했다는 것을 주의하십시오.
@Repository
public interface PlaceRepository extends PagingAndSortingRepository<Place, Long>,
    DistancePlaceRepository {
}
이것은 추가 인터페이스입니다. 우리는 우리의 거리 조회 방법이 여기에서 정의된 것을 볼 수 있습니다.
public interface DistancePlaceRepository {
    List<DistanceResult> findInDistance(Point center, double radiusMeters);
}
그런데 이런 방법은 어떻게 이루어졌을까?이 기능만 실현하는 상하문에 다른 @Repository bean을 추가할 수 있습니다
인터페이스Spring Data JPA는 실제로 표지 뒤에 혼합 마술을 합니다. 이렇게 하면 구체적인 저장소 bean은 잠시 후에 다음과 같이 사용됩니다.
@Repository
public class DistancePlaceRepositoryImpl implements DistancePlaceRepository {

    @PersistenceContext
    private EntityManager entityManager;

    @SuppressWarnings({"unchecked", "deprecation"})
    @Override
    public List<DistanceResult> findInDistance(Point center, double radiusMeters) {
        String jpql = "select p, distance(:center, p.location) from Place p where dwithin(:center, p.location, :radiusMeters) = true";

        return (List<DistanceResult>) entityManager.createQuery(jpql)
                .setParameter("center", center)
                .setParameter("radiusMeters", radiusMeters)
                .unwrap(Query.class)
                .setResultTransformer(
                        (ListResultTransformer)
                                (tuple, aliases) -> new DistanceResult(
                                        (Place) tuple[0],
                                        ((Number) tuple[1]).doubleValue()
                                )
                ).getResultList();
    }
}

I'm using Vlad Mihalcea's hibernate-types library here to get access to ListResultTransformer (See links below).


지금, 만약 우리가 이 변환기를 사용한다면, 조회 로그는 어떻게 될까요?
select place0_.id as col_0_0_, st_distance(?, place0_.location) as col_1_0_, place0_.id as id1_0_, place0_.location as location2_0_, place0_.name as name3_0_ from places place0_ where st_dwithin(?, place0_.location, ?)=true
보시다시피 그것은 단지 하나의 선택일 뿐입니다. 이것은 더욱 좋습니다.N+1이 보이지 않습니다.

결점


사용ResultTransformer을 시도할 때 가장 먼저 주의해야 할 것은 실제 사용 방법Query#setResultTransformer이 버려진 것으로 표시되어 있다는 것이다.이것은 실제로hibernate 개발자들의 지나친 반대이다.hibernate 6.0을 사용하지 않으면 추천하지 않는 방법을 사용할 수 없습니다.따라서 이 경고를 무시할 수 있음 (6.0을 GA로 억제한 다음 마이그레이션).
두 번째로 주의해야 할 것은 이 해결 방안은 분명히hibernate에 특정되어 있다는 것이다.ResultTransformer는 JPA(지금까지)의 등가물이 아니다.

솔루션 2: Blaze 지속성 솔리드 뷰


이 문제와 인터넷상의 관련 정보를 연구할 때 나는 창고 넘침answer을 만났고 이로 인해 이른바'Blaze 지속성 실체 보기'를 발견했다.이것으로 DTO를 재정의할 수 있을 것 같습니다.나는 그것을 연습으로 독자들에게 남겨 주어 그들documentation을 통해 우리가 어떻게 전체 실체의 포장을 정확하게 실현하고 원시 조회의 거리 부분을 비추는지 알게 했다.이것은 방대한 도서관이므로 나는 방법이 있다고 믿는다.

솔루션 3: jOOQ 사용


마지막으로 가장 중요하지 않은 점은 우리가 기존의 Hibernate를 소량jOOQ과 혼합하는 것을 막을 수 있는 것은 없다는 것이다.이것은 우리가 실행 중인 SQL을 완전히 제어할 수 있게 할 것이다.필요한 경우 DTO에 쉽게 패키징할 수 있습니다.물론 우리가 주의해야 할 것은 결과는 위탁 관리 실체가 아니지만, 우리의 용례에서 이 점은 우선 불필요한 것이다.
나는 이전에 공간 함수에 관심을 갖지 말라고 말했지만, 만약 우리가 정말 jOOQ를 사용하여 이런 검색을 하고 싶다면, 우리는 검색 생성기에서 이 SQL 함수를 지원해야 한다.다행히도github에는 jooq-postgis-spatial라는 프로젝트가 있습니다.

링크

  • Vlad Mihalcea는 hibernate 결과 변환기에서 Blog Post
  • Blaze 지속성 솔리드 뷰documentation
  • stackoverflow에 대한 원시적인 문제
  • jOOQhomepage
  • 사진작가Jason LeungUnsplash
  • 좋은 웹페이지 즐겨찾기