Spring/Spring Data JPA

나머지 기능들

민철킹 2021. 6. 17. 13:20

1. Specifications (명세)

 

Spring Data JPA는 JPA Criteria를 활용하여 명세의 개념을 사용할 수 있도록 지원(잘 사용하는 기능아님, 참고용)

 

명세란 도메인에 관한 규칙을 담는 도메인 객체로 객체가 특정 조건을 만족하는지를 판단하는 역할을 담당한다.

나중에 도메인 주도 설계(DDD)책을 한번 읽어보자!!

 

사용하고자 하는 곳에서 JpaSpecificationExecutor<엔티티타입>을 상속받아 사용하면 된다.

해당 인터페이스 내부에 들어가보면 findOne, findAll 등등 여러 메서드를 제공하는데 Parameter로 Specification이란 것을 넘긴다.

술어(predicate)

  • 참 또는 거짓으로 평가
  • AND OR 같은 연산자로 조합해서 다양한 검색조건을 쉽게 생성(컴포지트 패턴)
  • 예) 검색 조건 하나하나
  • 스프링 데이터 JPA는 org.springframework.data.jpa.domain.Specification 클래스로 정의

 

회원 이름 명세( username )와 팀 이름 명세( teamName )를 and 로 조합해서 검색 조건으로 사용(JPQL을 만들어줌)

where, and, or, not 등의 조건 사용가능

Criteria를 사용하는 것이 매우 복잡하기 때문에 잘 사용하지 않는 기능

머리 아퍼; 쓰지말자 그냥동적 쿼리를 위해선 QueryDSL!!

 


2. Query By Example

 

이름 그대로 Example에 의한 Query이다.

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

도메인 객체 자체를 검색조건으로 사용할 수 있다. 또한 검색 조건을 위한 객체를 위와 같이 생성하면 현재 이름만 설정되어 있기 때문에 Id 값에는 null이 들어간다(Long 타입이므로). null이면 검색 조건에서 무시를 하는데 age는 int 타입이기 때문에 age값이 0이다. 따라서 위와 같이 생성된 객체는 다음과 같은 정보를 가진다.

member = (null, "m1", 0)

즉, 이 member 정보가 검색 조건이 되어 이름이 m1이고 나이가 0살인 사람을 찾게 되는 것이다. 하지만 현재 저장된 회원 중에서 나이가 0살인 회원은 없기 때문에 검색되는 결과 리스트는 빈 리스트가 될 것이다.

이를 막기 위해 ExampleMatcher를 사용하여 매칭되는 조건 중 무시할 조건을 지정해줄 수 있다.

ExampleMatcher matcher = ExampleMatcher.matching()
                .withIgnorePaths("age");

 이를 도메인 객체와 함께 파라미터로 넘겨주면 원하는대로 m1이라는 이름을 가진 회원을 조회하게 된다.

 

 

Example을 파라미터로 넘길 수 있도록 Spring Data 기본 스펙에 추가가 되어 있기 때문에 위와 같이 사용이 가능하다.

또한 이 기능은 JpaRepository가 QueryByExample 기능을 하는 QueryByExampleExecutor를 상속받기 때문에 사용자가 JpaRepository만 상속받으면 사용가능하다.

 

추가로 위의 코드에 팀을 설정해주게 되면 member.team이 null이 아니므로 검색 조건에 추가되어

이름이 m1이고 팀이름이 teamA인 회원이 검색조건이 되게된다.(inner join) 

하지만 inner join만 가능하고 outer join은 불가능하지 않다는 한계가 존재한다. 복잡해지면 이 기능 사용 불가

 

장점

  • 동적 쿼리를 편리하게 처리
  • 도메인 객체를 그대로 사용
  • 데이터 저장소를 RDB에서 NOSQL로 변경해도 코드 변경이 없게 추상화 되어 있음
  • 스프링 데이터 JPA JpaRepository 인터페이스에 이미 포함

 

단점

  • 조인은 가능하지만 내부 조인(INNER JOIN)만 가능함 외부 조인(LEFT JOIN) 안됨
  • 다음과 같은 중첩 제약조건 안됨
    • firstname = ?0 or (firstname = ?1 and lastname = ?2)
  • 매칭 조건이 매우 단순함
    • 문자는 starts/contains/ends/regex
    • 다른 속성은 정확한 매칭( = )만 지원

 


3. Projections

 

엔티티 대신에 DTO를 편리하게 조회할 때 사용하는 기능이다.

ex: 전체 엔티티가 아니라 회원 이름만 조회하고 싶으면?

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projections

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

3-1. 인터페이스 기반의 Projections

원하는 getter의 property 형식으로 원하는 필드를 지정하여 인터페이스로 만든다.

이것을 사용할 리포지토리에 메서드를 만들어주는데 위에서는 예시로 메서드 명으로 쿼리를 만드는 방법을 사용하였다.

중요한 것은 List의 타입이 Member같은 엔티티가 아니라 우리가 만들어준 인터페이스로 하는 것이다.

위와 같이 만든 메서드를 사용하면 username만 조회할 수 있다.

실행된 쿼리문을 살펴보면 딱 username만 select한 것을 확인할 수 있다.

디버그를 통해 내부를 살펴보면

프록시 객체가 존재한다. 인터페이스를 정의하면 구현 클래스는 스프링 데이터 JPA가 프록시와 같은 가짜 객체를 만들어준다. findByUsername과 같은 메서드와 함께 조합해서 사용할 수 있다는 장점이 존재하므로 종종 사용되는 기능이다.

이는 인터페이스 기반의 Projection의 close Projection이고  그 외에 open Projection이라는 것이 있다.

@Value의 값으로 target의 속성을 가져와서 조합해 사용할 수도 있다. 여기서 사용된 target은 member를 의미한다. 

즉, 조회된 멤버의 이름 + 나이를 문자열로 만들어 getUsername에 넣어주는 것이다.(스프링 spel문법 지원)

엔티티를 전체 조회하고 거기서 원하는 데이터만 가져오는 식으로 동작한다.

 

 

3-2. 클래스 기반의 Projections

가져올 필드를 정의하고, 생성자를 만들어주는데 이 때 중요한 것은 생성자의 Parameter명이다. Parameter 이름으로 매칭을 시켜 Projection을 진행한다.

리스트 타입만 방금 만들어준 클래스로 변경해주면 완료

이 방법은 개발자가 구체적은 클래스를 명시해준 것이기 때문에 proxy가 필요하지 않다. 구체 클래스의 객체를 생성하고 생성자를 통해 값을 반환해준다.

 

3-3. 동적 Projection

쿼리가 같고 경우에 따라 나이만 또는 이름만 가져오고 싶을 때는 자바의 제네릭 타입을 사용하여 동적으로 사용 가능

사용할 때 parameter로 클래스 타입만 넘기면 동적으로 사용할 수 있다.

 

 

3-4. 중첩 Projection

연관된 다른 엔티티의 특정 값도 조회하고 싶을 때 사용

현재 제네릭 타입으로 메서드가 만들어져있기 때문에  클래스 타입만 갈아끼워주면된다.

실행된 쿼리문을 보면 root(member)는 원하는 데이터만 가져오도록 최적화가 되었지만 left join을 한 두번째 team은 엔티티를 전체 조회한 후 원하는 값만 출력한 것을 확인할 수 있다.

이것이 중첩 Projection에서의 단점이다. root 이외는 최적화가 되지 않는다.

프로젝션 대상이 root 엔티티면, JPQL SELECT 절 최적화 가능하지만 프로젝션 대상이 ROOT가 아니면 LEFT OUTER JOIN 처리 모든 필드를 SELECT해서 엔티티로 조회한 다음에 계산한다.

 

프로젝션 대상이 root 엔티티면 유용하다. 프로젝션 대상이 root 엔티티를 넘어가면 JPQL SELECT 최적화가 안된다!

복잡한 쿼리를 해결하기에는 한계가 있다.

 


4. Native Query

 

가급적이면 Native Query는 사용하지 않는 것이 가장 좋다. 최후의 수단으로 사용.

native한 sql문을 작성하고 nativeQuery 옵션을 true해주면 된다.

 

한계

  • 엔티티를 가져올 때 엔티티에 맞게 데이터를 전부 다 select 절에 적어줘야함
  • 반환 타입이 지원되는 것이 별로 없다. 만약 위의 상황에서 전체 조회(*)이 아니라 username만 가져온다면?
    • 반환 타입은 Member 엔티티인데 어떻게 처리할 것인가?
  • Sort 파라미터를 통한 정렬이 정상 동작하지 않을 수 있음(믿지 말고 직접 처리)
  • JPQL처럼 애플리케이션 로딩 시점에 문법 확인 불가
  • 동적 쿼리 불가

 

3-1. Projection 활용

네이티브 쿼리이지만 페이징도 가능하다. 네이티브 쿼리이기 때문에 count 쿼리를 꼭 작성해줘야한다.

 

 

반응형

'Spring > Spring Data JPA' 카테고리의 다른 글

Spring Data JPA 분석  (0) 2021.06.16
확장 기능  (0) 2021.06.15
JPA Hint, Lock  (0) 2021.06.14
@EntityGraph  (0) 2021.06.14
벌크성 수정 쿼리  (0) 2021.06.14