1. 概要

Spring Data JPA を使用して永続層を実装する場合、リポジトリは通常、ルートクラスの1つ以上のインスタンスを返します。 ただし、多くの場合、返されるオブジェクトのすべてのプロパティが必要なわけではありません。

このような場合、カスタマイズされたタイプのオブジェクトとしてデータを取得したい場合があります。 これらのタイプは、ルートクラスの部分的なビューを反映しており、関心のあるプロパティのみが含まれています。ここでプロジェクションが役立ちます。

2. 初期設定

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

2.1. Mavenの依存関係

依存関係については、このチュートリアルのセクション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
}

と:

@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を使用していることに注意してください。

組み込みデータベースが構成されると、SpringBootは定義したエンティティの基礎となるテーブルを自動的に生成します。

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);
}

プロジェクションインターフェイスを使用してリポジトリメソッドを定義することは、エンティティクラスを使用する場合とほとんど同じであることが簡単にわかります。

唯一の違いは、返されたコレクションの要素タイプとして、エンティティクラスではなくプロジェクションインターフェイスが使用されることです。

アドレスプロジェクションの簡単なテストをしてみましょう。

@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");

再帰的射影は、所有側から逆側にトラバースする場合にのみ機能することに注意してください。逆の場合、ネストされた射影はnullに設定されます。

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;

@Test 
public void whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned() {
    PersonView personView = personRepository.findByLastName("Doe");
 
    assertThat(personView.getFullName()).isEqualTo("John Doe");
}

ただし、オープンプロジェクションには欠点があります。 Spring Dataは、使用されるプロパティが事前にわからないため、クエリの実行を最適化できません。 したがって、クローズドプロジェクションが要件を処理できない場合にのみ、オープンプロジェクションを使用する必要があります。

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 DataJPAプロジェクションについて説明しました。

この記事のソースコードは、GitHubから入手できます。 これはMavenプロジェクトであり、そのまま実行できるはずです。