본문 바로가기

Web/spring

[Spring data] JPA repositories - Projection

728x90

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

 

Spring Data JPA - Reference Documentation

Example 119. 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

 

잘못된 내용이 있다면 알려주세요 🤔

 


 

 

Projections

 

Spring Data query method는 루트나 여러 인스턴스를 반환할 수 있다.

이런 type의 특정 특성을 기반으로 projection을 만들면 전용 return type을 모델링할 수 있다

 

예제코드

class Person {

  @Id UUID id;
  String firstname, lastname;
  Address address;

  static class Address {
    String zipCode, city, street;
  }
}

interface PersonRepository extends Repository<Person, UUID> {

  Collection<Person> findByLastname(String lastname);
}

 

만약 여기서 실제 필요한 내용이 이름뿐이라면

또는 이름을 제외한 나머지를 보안상 가져오고 싶지 않다면 어떻게 해야할까?

 

Interface-based Projections

 

첫번째 방법은 인터페이스를 선언하여 가져오는 것이다

interface NamesOnly {

  String getFirstname();
  String getLastname();
}

 

이렇게 repository 를 만들면된다

interface PersonRepository extends Repository<Person, UUID> {

  Collection<NamesOnly> findByLastname(String lastname);
}

 

The query execution engine creates proxy instances of that interface at runtime for each element returned and forwards calls to the exposed methods to the target object.

 

default method를 재정의하는 method가 선언되면 Repository는 선언된 return type과 관계없이 기본 메서드가 호출된다

projection에서는 이 기본 메서드를 사용할 수 없고 호환된 반환 type을 사용해야한다

@Query 로 재정의할 수 있도록 지원한다

 

 

사용하다보니 도시 정보도 필요하다고 한다.

그러면 Projection을 recursively하게 사용하면 된다

 

interface PersonSummary {

  String getFirstname();
  String getLastname();
  AddressSummary getAddress();

  interface AddressSummary {
    String getCity();
  }
}
 
이러한 형태는 A closed projection으로 Spring Data에서 projection proxy를 지원할 때 필요한 특성으로 쿼리 실행을 최적화 할 수 있다
 
open projection의 예시는 이러한 코드이다
interface NamesOnly {

  @Value("#{target.firstname + ' ' + target.lastname}")
  String getFullName();
  …
}

해당 target 변수에 접근할 수 있기 때문에 해당 인터페이스는 @Value를 사용한 Open projection이 된다

쿼리 실행 최적화에 적용할 수 없다

그렇기 때문에 이런 방식 보다는

 

closed projection을 사용하고 커스텀 로직을 작성하는게 좋다

 

A projection interface using a default method for custom logic
interface NamesOnly {

  String getFirstname();
  String getLastname();

  default String getFullName() {
    return getFirstname().concat(" ").concat(getLastname());
  }
}

 

조금더 유연하게 사용하려면 이런 방법을 쓴다

 

Sample Person object
@Component
class MyBean {

  String getFullName(Person person) {
    …
  }
}

interface NamesOnly {

  @Value("#{@myBean.getFullName(target)}")
  String getFullName();
  …
}

 

 

 

메서드 매개 변수는 args라는 개체 배열을 통해 사용할 수 있다.

args 배열에서 메소드 매개변수를 가져오는 방법이다.

 

Sample Person object
interface NamesOnly {

  @Value("#{args[0] + ' ' + target.firstname + '!'}")
  String getSalutation(String prefix);
}

 

*** 복잡한 표현식은 Spring Bean을 사용해야하며, 표현식으로 method를 호출해야한다

 

 

Nullable wrapper 를 사용하면 null 안전성을 향상시킨다

옵셔널을 사용하면 된다

  • java.util.Optional
  • com.google.common.base.Optional
  • scala.Option
  • io.vavr.control.Option
 
 
A projection interface using nullable wrappers
interface NamesOnly {

  Optional<String> getFirstname();
}​
 
값이 없다면 null을 반환하고, Optional에서 널값인지를 체크하여 분기처리하면 된다
 
 
 
 
 
Class-based Projections (DTOs)
 
DTO는 많은 사람들이 예제에서 사용하기 때문에 알겠지만, 필드의 속성을 보유하는 Data Transfer Objects 의 약자이다
이러한 유형은 proxing이 발생하지 않고, same way projection을 사용할 수 없다는 점을 제외하고 위의 인터페이스와 동일한 방식으로 사용할 수 있다
 

If the store optimizes the query execution by limiting the fields to be loaded, the fields to be loaded are determined from the parameter names of the constructor that is exposed.

 

A projecting DTO
class NamesOnly {

  private final String firstname, lastname;

  NamesOnly(String firstname, String lastname) {

    this.firstname = firstname;
    this.lastname = lastname;
  }

  String getFirstname() {
    return this.firstname;
  }

  String getLastname() {
    return this.lastname;
  }

  // equals(…) and hashCode() implementations
}
 
 
Lombok을 사용하면 코드를 엄청 단순화 시킬 수 있다
Spring @Value 가 아니라 Project Lombok 주석 @Value 애노테이션을 붙이면 아주 간결하게 위의 코드와 같게 만들 수 있다
 
@Value
class NamesOnly {
	String firstname, lastname;
}

Fields are private final by default, and the class exposes a constructor that takes all fields and automatically gets equals(…) and hashCode() methods implemented.

 

 

Dynamic Projections

 

지금까지는 return typeEhsms dyth dbguddmfh projection type을 사용하고 있었으나

호출 시에 사용할 유형을 선택할 수도 있다(which makes it dynamic)

 

A repository using a dynamic projection parameter
interface PersonRepository extends Repository<Person, UUID> {

  <T> Collection<T> findByLastname(String lastname, Class<T> type);
}

 

Using a repository with dynamic projections
void someMethod(PersonRepository people) {

  Collection<Person> aggregates =
    people.findByLastname("Matthews", Person.class);

  Collection<NamesOnly> aggregates =
    people.findByLastname("Matthews", NamesOnly.class);
}

타입 지정해서 받기, 이거 너무 좋은데? 한번 나도 써봐야겠다

 

Query parameters of type Class are inspected whether they qualify as dynamic projection parameter. 

If the actual return type of the query equals the generic parameter type of the Class parameter, then the matching Class parameter is not available for usage within the query or SpEL expressions. 

If you want to use a Class parameter as query argument then make sure to use a different generic parameter, for example Class<?>.

 

 

728x90