Spring Data JPAプロジェクション

1. 概要

link:/the-persistence-layer-with-spring-and-jpa[Spring Data JPA]を使用して永続層を実装する場合、リポジトリは通常、ルートクラスの1つ以上のインスタンスを返します。 ただし、多くの場合、返されるオブジェクトのすべてのプロパティは必要ありません。
このような場合、カスタマイズされたタイプのオブジェクトとしてデータを取得することが望ましい場合があります。 *これらのタイプは、関心のあるプロパティのみを含むルートクラスの部分ビューを反映しています。*これは、投影が便利な場所です。

2. 初期設定

最初のステップは、プロジェクトをセットアップし、データベースに入力することです。

* 2.1。 Mavenの依存関係*

依存関係については、https://www.baeldung.com/spring-data-case-insensitive-queries [このチュートリアル]のセクション2をご覧ください。

* 2.2。 エンティティクラス*

2つのエンティティクラスを定義しましょう。
@Entity
public class Address {

    @Id
    private Long id;

    @OneToOne
    private Person person;

    private String state;

    private String city;

    private String street;

    private String zipCode;

    // getters and setters
}
And:
@Entity
public class Person {

    @Id
    private Long id;

    private String firstName;

    private String lastName;

    @OneToOne(mappedBy = "person")
    private Address address;

    // getters and setters
}
_Person_エンティティと_Address_エンティティの関係は、1対1の双方向です。_Address_は所有側で、_Person_は逆側です。
このチュートリアルでは、組み込みデータベース— H2を使用しています。
*組み込みデータベースが設定されると、Spring Bootは定義したエンティティの基になるテーブルを自動的に生成します。*

* 2.3。 SQLスクリプト*

_projection-insert-data.sql_スクリプトを使用して、両方のバッキングテーブルにデータを入力します。
INSERT INTO person(id,first_name,last_name) VALUES (1,'John','Doe');
INSERT INTO address(id,person_id,state,city,street,zip_code)
  VALUES (1,1,'CA', 'Los Angeles', 'Standford Ave', '90001');
各テストの実行後にデータベースをクリーンアップするには、_projection-clean-up-data.sql_という名前の別のスクリプトを使用できます。
DELETE FROM address;
DELETE FROM person;

2.4. テストクラス

投影が正しいデータを生成することを確認するには、テストクラスが必要です。
@DataJpaTest
@RunWith(SpringRunner.class)
@Sql(scripts = "/projection-insert-data.sql")
@Sql(scripts = "/projection-clean-up-data.sql", executionPhase = AFTER_TEST_METHOD)
public class JpaProjectionIntegrationTest {
    // injected fields and test methods
}
指定された注釈を使用して、* Spring Bootはデータベースを作成し、依存関係を注入し、各テストメソッドの実行前後にテーブルを作成してクリーンアップします。*

3. インターフェイスベースのプロジェクション

エンティティを投影するとき、実装を提供する必要がないため、インターフェイスに依存するのが自然です。

* 3.1。 クローズドプロジェクション*

_Address_クラスを振り返ると、*多くのプロパティがありますが、すべてが役立つわけではありません*。たとえば、郵便番号で住所を示すのに十分な場合があります。
_Address_クラスのプロジェクションインターフェイスを宣言しましょう。
public interface AddressView {
    String getZipCode();
}
次に、リポジトリインターフェイスで使用します。
public interface AddressRepository extends Repository<Address, Long> {
    List<AddressView> getAddressByState(String state);
}
プロジェクションインターフェイスを使用してリポジトリメソッドを定義することは、エンティティクラスを使用する場合とほとんど同じであることがわかります。
唯一の違いは、*エンティティクラスではなく投影インターフェイスが、返されるコレクションの要素タイプとして使用されることです*。
_Address_プロジェクションの簡単なテストを行ってみましょう。
@Autowired
private AddressRepository addressRepository;

@Test
public void whenUsingClosedProjections_thenViewWithRequiredPropertiesIsReturned() {
    AddressView addressView = addressRepository.getAddressByState("CA").get(0);
    assertThat(addressView.getZipCode()).isEqualTo("90001");
    // ...
}
舞台裏では、* Springは各エンティティオブジェクトの投影インターフェイスのプロキシインスタンスを作成し、プロキシへのすべての呼び出しはそのオブジェクトに転送されます。*
プロジェクションを再帰的に使用できます。 たとえば、_Person_クラスの投影インターフェイスは次のとおりです。
public interface PersonView {
    String getFirstName();

    String getLastName();
}
次に、_Address_プロジェクションに戻り型_PersonView_(ネストされたプロジェクション)を持つメソッドを追加しましょう。
public interface AddressView {
    // ...
    PersonView getPerson();
}
*ネストされた投影を返すメソッドは、関連するエンティティを返すルートクラスのメソッドと同じ名前である必要があります。*
作成したテストメソッドにいくつかのステートメントを追加して、ネストされたプロジェクションを検証しましょう。
// ...
PersonView personView = addressView.getPerson();
assertThat(personView.getFirstName()).isEqualTo("John");
assertThat(personView.getLastName()).isEqualTo("Doe");
*再帰投影は、所有側から逆側に移動する場合にのみ機能することに注意してください。

* 3.2。 オープンプロジェクション*

ここまでは、閉じたプロジェクションを行ってきました。これは、メソッドがエンティティプロパティの名前と完全に一致するプロジェクションインターフェイスを示しています。
別の種類のインターフェイスベースの投影法があります。オープン投影法です。 *これらのプロジェクションにより、名前が一致せず、実行時に計算される戻り値を持つインターフェイスメソッドを定義できます。*
_Person_プロジェクションインターフェイスに戻り、新しいメソッドを追加しましょう。
public interface PersonView {
    // ...

    @Value("#{target.firstName + ' ' + target.lastName}")
    String getFullName();
}
_ @ Value_アノテーションの引数はSpEL式であり、_target_指定子はバッキングエンティティオブジェクトを示します。
次に、別のリポジトリインターフェイスを定義します。
public interface PersonRepository extends Repository<Person, Long> {
    PersonView findByLastName(String lastName);
}
簡単にするために、コレクションではなく単一の投影オブジェクトのみを返します。
このテストでは、開いている投影が期待どおりに機能することを確認します。
@Autowired
private PersonRepository personRepository;

@Testpublic void whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned() {
    PersonView personView = personRepository.findByLastName("Doe");

    assertThat(personView.getFullName()).isEqualTo("John Doe");
}
オープンプロジェクションには欠点があります。SpringDataは、使用されるプロパティが事前にわからないため、クエリの実行を最適化できません。 したがって、*閉じた投影では要件を処理できない場合にのみ、開いた投影を使用する必要があります。*

4. クラスベースのプロジェクション

Spring Dataが投影インターフェースから作成するプロキシを使用する代わりに、*独自の投影クラスを定義できます*。
たとえば、_Person_エンティティの投影クラスは次のとおりです。
public class PersonDto {
    private String firstName;
    private String lastName;

    public PersonDto(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    // getters, equals and hashCode
}
*プロジェクションクラスをリポジトリインターフェイスと連携させるには、そのコンストラクターのパラメーター名がルートエンティティークラスのプロパティと一致する必要があります。*
_equals_と_hashCode_の実装も定義する必要があります。これにより、Spring Dataはコレクション内の投影オブジェクトを処理できます。
それでは、_Person_リポジトリにメソッドを追加しましょう。
public interface PersonRepository extends Repository<Person, Long> {
    // ...

    PersonDto findByFirstName(String firstName);
}
このテストは、クラスベースの投影を検証します。
@Test
public void whenUsingClassBasedProjections_thenDtoWithRequiredPropertiesIsReturned() {
    PersonDto personDto = personRepository.findByFirstName("John");

    assertThat(personDto.getFirstName()).isEqualTo("John");
    assertThat(personDto.getLastName()).isEqualTo("Doe");
}
*クラスベースのアプローチでは、ネストされたプロジェクションを使用できません。*

5. 動的投影

エンティティクラスには多くのプロジェクションがあります。 特定のタイプを使用する場合もありますが、別のタイプが必要な場合もあります。 場合によっては、エンティティクラス自体も使用する必要があります。
複数の戻り値型をサポートするためだけに個別のリポジトリインターフェイスまたはメソッドを定義するのは面倒です。 この問題に対処するために、Spring Dataはより優れたソリューションである動的投影を提供します。
  • _Class_パラメータを使用してリポジトリメソッドを宣言するだけで、動的投影を適用できます。*

public interface PersonRepository extends Repository<Person, Long> {
    // ...

    <T> T findByLastName(String lastName, Class<T> type);
}
このようなメソッドに投影タイプまたはエンティティクラスを渡すことにより、目的のタイプのオブジェクトを取得できます。
@Test
public void whenUsingDynamicProjections_thenObjectWithRequiredPropertiesIsReturned() {
    Person person = personRepository.findByLastName("Doe", Person.class);
    PersonView personView = personRepository.findByLastName("Doe", PersonView.class);
    PersonDto personDto = personRepository.findByLastName("Doe", PersonDto.class);

    assertThat(person.getFirstName()).isEqualTo("John");
    assertThat(personView.getFirstName()).isEqualTo("John");
    assertThat(personDto.getFirstName()).isEqualTo("John");
}

6. 結論

この記事では、さまざまなタイプのSpring Data JPAプロジェクションについて検討しました。
このチュートリアルのソースコードは、https://github.com/eugenp/tutorials/tree/master/persistence-modules/spring-data-jpa-2 [GitHub上]で入手できます。 これはMavenプロジェクトであり、そのまま実行できるはずです。