Spring Data 拡張
このセクションでは、さまざまなコンテキストで Spring Data を使用できるようにする一連の Spring Data 拡張について説明します。現在、ほとんどの統合は Spring MVC を対象としています。
Querydsl 拡張
Querydsl (英語) は、流れるような API を使用して、静的に型指定された SQL のようなクエリの構築を可能にするフレームワークです。
Querydsl のメンテナンスが遅くなり、コミュニティがプロジェクトを OpenFeign の github.com/OpenFeign/querydsl (英語) (groupId io.github.openfeign.querydsl ) にフォークしました。Spring Data はベストエフォート方式でフォークをサポートしています。 |
次の例に示すように、いくつかの Spring Data モジュールは、QuerydslPredicateExecutor
を介して Querydsl との統合を提供します。
public interface QuerydslPredicateExecutor<T> {
Optional<T> findById(Predicate predicate); (1)
Iterable<T> findAll(Predicate predicate); (2)
long count(Predicate predicate); (3)
boolean exists(Predicate predicate); (4)
// … more functionality omitted.
}
1 | Predicate に一致する単一のエンティティを検索して返します。 |
2 | Predicate に一致するすべてのエンティティを検索して返します。 |
3 | Predicate に一致するエンティティの数を返します。 |
4 | Predicate に一致するエンティティが存在するかどうかを返します。 |
Querydsl サポートを使用するには、次の例に示すように、リポジトリインターフェースで QuerydslPredicateExecutor
を継承します。
interface UserRepository extends CrudRepository<User, Long>, QuerydslPredicateExecutor<User> {
}
前の例では、次の例に示すように、Querydsl Predicate
インスタンスを使用して型安全なクエリを記述できます。
Predicate predicate = user.firstname.equalsIgnoreCase("dave")
.and(user.lastname.startsWithIgnoreCase("mathews"));
userRepository.findAll(predicate);
Web サポート
リポジトリプログラミングモデルをサポートする Spring Data モジュールには、さまざまな Web サポートが付属しています。Web 関連のコンポーネントでは、Spring MVC JAR がクラスパス上にある必要があります。それらのいくつかは、Spring HATEOAS [GitHub] (英語) との統合さえ提供します。一般に、統合サポートは、次の例に示すように、JavaConfig 構成クラスで @EnableSpringDataWebSupport
アノテーションを使用することで有効になります。
Java
XML
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />
<!-- If you use Spring HATEOAS, register this one *instead* of the former -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />
@EnableSpringDataWebSupport
アノテーションは、いくつかのコンポーネントを登録します。これらについては、このセクションの後半で説明します。また、クラスパスで Spring HATEOAS を検出し、統合コンポーネント(存在する場合)も登録します。
基本的な Web サポート
前のセクションで示した構成は、いくつかの基本的なコンポーネントを登録します。
Spring MVC がリクエストパラメーターまたはパス変数からリポジトリ管理ドメインクラスのインスタンスを解決できるようにする
DomainClassConverter
クラスの使用。Spring MVC がリクエストパラメーターから
Pageable
およびSort
インスタンスを解決できるようにするHandlerMethodArgumentResolver
実装。Jackson モジュールは、使用する Spring Data モジュールに応じて、
Point
やDistance
などの型を逆 / 直列化するか、特定の型を格納します。
DomainClassConverter
クラスの使用
DomainClassConverter
クラスを使用すると、Spring MVC コントローラーメソッドシグネチャーでドメイン型を直接使用できるため、次の例に示すように、リポジトリからインスタンスを手動で検索する必要がありません。
@Controller
@RequestMapping("/users")
class UserController {
@RequestMapping("/{id}")
String showUserForm(@PathVariable("id") User user, Model model) {
model.addAttribute("user", user);
return "userForm";
}
}
このメソッドは User
インスタンスを直接受け取り、それ以上のルックアップは必要ありません。インスタンスは、Spring MVC が最初にパス変数をドメインクラスの id
型に変換し、最終的にドメイン型に登録されたリポジトリインスタンスで findById(…)
を呼び出してインスタンスにアクセスすることで解決できます。
現在、リポジトリは変換のために発見される資格があるために CrudRepository を実装しなければなりません。 |
ページング可能およびソート用の HandlerMethodArgumentResolvers
前のセクションで示した構成スニペットは、PageableHandlerMethodArgumentResolver
と SortHandlerMethodArgumentResolver
のインスタンスも登録します。次の例に示すように、登録により、Pageable
および Sort
が有効なコントローラーメソッド引数として有効になります。
@Controller
@RequestMapping("/users")
class UserController {
private final UserRepository repository;
UserController(UserRepository repository) {
this.repository = repository;
}
@RequestMapping
String showUsers(Model model, Pageable pageable) {
model.addAttribute("users", repository.findAll(pageable));
return "users";
}
}
上記のメソッドシグネチャーにより、Spring MVC は、次のデフォルト構成を使用して、リクエストパラメーターから Pageable
インスタンスを派生させようとします。
| 取得するページ。0 からインデックス付けされ、デフォルトは 0 です。 |
| 取得するページのサイズ。デフォルトは 20 です。 |
|
|
この動作をカスタマイズするには、PageableHandlerMethodArgumentResolverCustomizer
インターフェースまたは SortHandlerMethodArgumentResolverCustomizer
インターフェースをそれぞれ実装する Bean を登録します。次の例に示すように、customize()
メソッドが呼び出され、設定を変更できます。
@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
return s -> s.setPropertyDelimiter("<-->");
}
既存の MethodArgumentResolver
のプロパティを設定するだけでは目的に合わない場合は、SpringDataWebConfiguration
または HATEOAS 対応の拡張機能を継承し、pageableResolver()
または sortResolver()
メソッドをオーバーライドし、@Enable
アノテーションを使用する代わりにカスタマイズした構成ファイルをインポートします。
リクエストから複数の Pageable
または Sort
インスタンスを解決する必要がある場合(たとえば、複数のテーブルの場合)、Spring の @Qualifier
アノテーションを使用して互いに区別できます。次に、リクエストパラメーターの前に ${qualifier}_
を付ける必要があります。次の例は、結果のメソッドシグネチャーを示しています。
String showUsers(Model model,
@Qualifier("thing1") Pageable first,
@Qualifier("thing2") Pageable second) { … }
thing1_page
、thing2_page
などを設定する必要があります。
メソッドに渡されるデフォルトの Pageable
は PageRequest.of(0, 20)
と同等ですが、Pageable
パラメーターの @PageableDefault
アノテーションを使用してカスタマイズできます。
Page
の JSON 表現の作成
Spring MVC コントローラーが最終的に Spring Data ページの表現をクライアントにレンダリングしようとするのは一般的です。単純にハンドラーメソッドから Page
インスタンスを返して、Jackson にそのままレンダリングさせることもできますが、基になる実装クラス PageImpl
はドメイン型であるため、これは強くお勧めしません。これは、関係のない理由で API を変更する必要がある、または変更する必要がある可能性があり、そのような変更により結果として得られる JSON 表現が破壊的なメソッドで変更される可能性があることを意味します。
Spring Data 3.1 では、問題を説明する警告ログを発行することで問題を提案し始めました。最終的には、完全に安定したハイパーメディア対応の方法でページをレンダリングし、クライアントが簡単にページを移動できるようにするために、Spring HATEOAS との統合を活用することをお勧めします。ただし、バージョン 3.3 Spring Data には、使いやすいページレンダリングメカニズムが付属していますが、Spring HATEOAS を含める必要はありません。
Spring Data' PagedModel
を使用する
このサポートの中核は、Spring HATEOAS PagedModel
の簡易バージョン (org.springframework.data.web
パッケージに含まれる Spring Data バージョン) で構成されます。これを使用して Page
インスタンスをラップすると、Spring HATEOAS によって確立された構造を反映しながらナビゲーションリンクを省略した単純化された表現が得られます。
import org.springframework.data.web.PagedModel;
@Controller
class MyController {
private final MyRepository repository;
// Constructor ommitted
@GetMapping("/page")
PagedModel<?> page(Pageable pageable) {
return new PagedModel<>(repository.findAll(pageable)); (1)
}
}
1 | Page インスタンスを PagedModel にラップします。 |
これにより、次のような JSON 構造が得られます。
{
"content" : [
… // Page content rendered here
],
"page" : {
"size" : 20,
"totalElements" : 30,
"totalPages" : 2,
"number" : 0
}
}
重要なページネーションのメタデータを公開する page
フィールドがドキュメントにどのように含まれているかに注目してください。
簡素化された Page
レンダリングをグローバルに有効にする
既存のコントローラーをすべて変更して、Page
ではなく PagedModel
を返すマッピングステップを追加したくない場合は、次のように @EnableSpringDataWebSupport
を調整することで、PageImpl
インスタンスの PagedModel
への自動変換を有効にすることができます。
@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)
class MyConfiguration { }
これにより、コントローラーは引き続き Page
インスタンスを返すことができ、それらは自動的に簡略化された表現にレンダリングされます。
@Controller
class MyController {
private final MyRepository repository;
// Constructor ommitted
@GetMapping("/page")
Page<?> page(Pageable pageable) {
return repository.findAll(pageable);
}
}
Page
および Slice
のハイパーメディアサポート
Spring HATEOAS には、Page
または Slice
インスタンスのコンテンツを必要な Page
/Slice
メタデータとリンクで強化して、クライアントがページを簡単にナビゲートできるようにする表現モデルクラス (PagedModel
/SlicedModel
) が付属しています。Page
から PagedModel
への変換は、PagedResourcesAssembler
と呼ばれる Spring HATEOAS RepresentationModelAssembler
インターフェースの実装によって行われます。同様に、Slice
インスタンスは SlicedResourcesAssembler
を使用して SlicedModel
に変換できます。次の例は、SlicedResourcesAssembler
がまったく同じように機能するため、PagedResourcesAssembler
をコントローラーメソッドの引数として使用する方法を示しています。
@Controller
class PersonController {
private final PersonRepository repository;
// Constructor omitted
@GetMapping("/people")
HttpEntity<PagedModel<Person>> people(Pageable pageable,
PagedResourcesAssembler assembler) {
Page<Person> people = repository.findAll(pageable);
return ResponseEntity.ok(assembler.toModel(people));
}
}
前の例に示すように、構成を有効にすると、PagedResourcesAssembler
をコントローラーメソッドの引数として使用できます。その上で toModel(…)
を呼び出すと、次の効果があります。
Page
のコンテンツは、PagedModel
インスタンスのコンテンツになります。PagedModel
オブジェクトはPageMetadata
インスタンスをアタッチし、Page
および基礎となるPageable
からの情報が取り込まれます。PagedModel
には、ページの状態に応じて、prev
およびnext
リンクが添付される場合があります。リンクは、メソッドがマップする URI を指します。メソッドに追加されたページネーションパラメーターは、PageableHandlerMethodArgumentResolver
の設定と一致して、リンクを後で解決できるようにします。
データベースに 30 個の Person
インスタンスがあると仮定します。これで、リクエスト(GET localhost:8080/people
)をトリガーして、次のような出力を確認できます。
{ "links" : [
{ "rel" : "next", "href" : "http://localhost:8080/persons?page=1&size=20" }
],
"content" : [
… // 20 Person instances rendered here
],
"page" : {
"size" : 20,
"totalElements" : 30,
"totalPages" : 2,
"number" : 0
}
}
ここに示されている JSON エンベロープ形式は、正式に指定された構造に従っておらず、安定しているとは保証されておらず、いつでも変更される可能性があります。HAL などの Spring HATEOAS でサポートされている、ハイパーメディア対応の公式メディア型としてレンダリングを有効にすることを強くお勧めします。これらは、@EnableHypermediaSupport アノテーションを使用してアクティブ化できます。詳細については、Spring HATEOAS リファレンスドキュメントを参照してください。 |
アセンブラーは正しい URI を生成し、デフォルト構成を選択して、パラメーターを次のリクエストの Pageable
に解決しました。つまり、その構成を変更すると、リンクは自動的に変更に準拠します。デフォルトでは、アセンブラーはそれが呼び出されたコントローラーメソッドを指しますが、ページネーションリンクを構築するためのベースとして使用されるカスタム Link
を渡すことにより、それをカスタマイズできます。これにより、PagedResourcesAssembler.toModel(…)
メソッドがオーバーロードされます。
Spring Data Jackson モジュール
コアモジュール、および一部のストア固有のモジュールには、Spring Data ドメインで使用される org.springframework.data.geo.Distance
や org.springframework.data.geo.Point
などの型の Jackson モジュールのセットが付属しています。
これらのモジュールは、Web サポートが有効になり、com.fasterxml.jackson.databind.ObjectMapper
が使用可能になるとインポートされます。
初期化中に、SpringDataJacksonConfiguration
と同様に SpringDataJacksonModules
がインフラストラクチャによって取得されるため、宣言された com.fasterxml.jackson.databind.Module
が Jackson ObjectMapper
で使用できるようになります。
次のドメイン型のデータバインディングミックスインは、共通のインフラストラクチャによって登録されます。
org.springframework.data.geo.Distance org.springframework.data.geo.Point org.springframework.data.geo.Box org.springframework.data.geo.Circle org.springframework.data.geo.Polygon
個々のモジュールは、追加の |
Web データバインディングのサポート
次の例に示すように、Spring Data 射影(射影で説明)を使用して、JSONPath (英語) 式(Jayway JsonPath [GitHub] (英語) が必要)または XPath [W3C] (英語) 式(XmlBeam (英語) が必要)のいずれかを使用して、受信リクエストペイロードをバインドできます。
@ProjectedPayload
public interface UserPayload {
@XBRead("//firstname")
@JsonPath("$..firstname")
String getFirstname();
@XBRead("/lastname")
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}
前の例に示されている型は、Spring MVC ハンドラーメソッドの引数として使用するか、RestTemplate
のいずれかのメソッドで ParameterizedTypeReference
を使用することで使用できます。上記のメソッド宣言は、指定されたドキュメント内の任意の場所で firstname
を見つけようとします。lastname
XML ルックアップは、受信ドキュメントのトップレベルで実行されます。の JSON バリアントは、トップレベルの lastname
を最初に試行しますが、前者が値を返さない場合は、user
サブドキュメントにネストされた lastname
も試行します。こうすることで、クライアントが公開メソッドを呼び出さなくても、ソースドキュメントの構造の変更を簡単に軽減できます (通常、クラスベースのペイロードバインディングの欠点です)。
ネストされた射影は、射影に従ってサポートされます。メソッドがインターフェース以外の複雑な型を返す場合、Jackson ObjectMapper
が最終値のマッピングに使用されます。
Spring MVC の場合、@EnableSpringDataWebSupport
がアクティブになり、必要な依存関係がクラスパスで使用可能になるとすぐに、必要なコンバーターが自動的に登録されます。RestTemplate
で使用する場合は、手動で ProjectingJackson2HttpMessageConverter
(JSON) または XmlBeamHttpMessageConverter
を登録します。
詳細については、標準の Spring Data サンプルリポジトリ [GitHub] (英語) の Web 射影の例 [GitHub] (英語) を参照してください。
Querydsl Web サポート
Querydsl (英語) が統合されているストアの場合、Request
クエリ文字列に含まれている属性からクエリを派生させることができます。
次のクエリ文字列を検討してください。
?firstname=Dave&lastname=Matthews
前の例の User
オブジェクトが与えられた場合、次のように QuerydslPredicateArgumentResolver
を使用して、クエリ文字列を次の値に解決できます。
QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))
Querydsl がクラスパスで見つかると、この機能は @EnableSpringDataWebSupport とともに自動的に有効になります。 |
メソッドシグネチャーに @QuerydslPredicate
を追加すると、すぐに使用できる Predicate
が提供されます。これは、QuerydslPredicateExecutor
を使用して実行できます。
型情報は通常、メソッドの戻り値型から解決されます。その情報は必ずしもドメイン型と一致しないため、QuerydslPredicate の root 属性を使用することをお勧めします。 |
次の例は、メソッドシグネチャーで @QuerydslPredicate
を使用する方法を示しています。
@Controller
class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate, (1)
Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {
model.addAttribute("users", repository.findAll(predicate, pageable));
return "index";
}
}
1 | User の一致する Predicate にクエリ文字列引数を解決します。 |
デフォルトのバインディングは次のとおりです。
eq
としての単純なプロパティのObject
。contains
のようなプロパティのようなコレクションのObject
。in
としての単純なプロパティのCollection
。
これらのバインディングは、@QuerydslPredicate
の bindings
属性を使用するか、Java 8 default methods
を使用して、次のようにリポジトリインターフェースに QuerydslBinderCustomizer
メソッドを追加することによってカスタマイズできます。
interface UserRepository extends CrudRepository<User, String>,
QuerydslPredicateExecutor<User>, (1)
QuerydslBinderCustomizer<QUser> { (2)
@Override
default void customize(QuerydslBindings bindings, QUser user) {
bindings.bind(user.username).first((path, value) -> path.contains(value)) (3)
bindings.bind(String.class)
.first((StringPath path, String value) -> path.containsIgnoreCase(value)); (4)
bindings.excluding(user.password); (5)
}
}
1 | QuerydslPredicateExecutor は、Predicate の特定のファインダーメソッドへのアクセスを提供します。 |
2 | リポジトリインターフェースで定義された QuerydslBinderCustomizer が自動的に選択され、ショートカット @QuerydslPredicate(bindings=…) が選択されます。 |
3 | username プロパティのバインディングを単純な contains バインディングとして定義します。 |
4 | String プロパティのデフォルトのバインディングを、大文字と小文字を区別しない contains 一致になるように定義します。 |
5 | password プロパティを Predicate 解決から除外します。 |
リポジトリまたは @QuerydslPredicate から特定のバインディングを適用する前に、デフォルトの Querydsl バインディングを保持する QuerydslBinderCustomizerDefaults Bean を登録できます。 |
リポジトリポピュレーター
Spring JDBC モジュールを使用している場合は、おそらく DataSource
に SQL スクリプトを取り込むためのサポートに精通しているでしょう。同様の抽象化がリポジトリレベルで利用できますが、ストアに依存しない必要があるため、データ定義言語として SQL を使用しません。ポピュレーターは XML(Spring の OXM 抽象化による)と JSON(Jackson による)をサポートして、リポジトリにデータを取り込むデータを定義します。
次の内容の data.json
というファイルがあるとします。
[ { "_class" : "com.acme.Person",
"firstname" : "Dave",
"lastname" : "Matthews" },
{ "_class" : "com.acme.Person",
"firstname" : "Carter",
"lastname" : "Beauford" } ]
Spring Data Commons で提供されるリポジトリ名前空間の populator 要素を使用して、リポジトリにデータを取り込むことができます。上記のデータを PersonRepository
に入力するには、次のようなポピュレーターを宣言します。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:repository="http://www.springframework.org/schema/data/repository"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/repository
https://www.springframework.org/schema/data/repository/spring-repository.xsd">
<repository:jackson2-populator locations="classpath:data.json" />
</beans>
上記の宣言により、data.json
ファイルは Jackson ObjectMapper
によって読み取られ、逆直列化されます。
JSON オブジェクトが非整列化される型は、JSON ドキュメントの _class
属性を調べることで決定されます。インフラストラクチャは最終的に、適切なリポジトリを選択して、デシリアライズされたオブジェクトを処理します。
代わりに、XML を使用してリポジトリにデータを取り込む必要のあるデータを定義するには、unmarshaller-populator
エレメントを使用できます。Spring OXM で使用可能な XML マーシャラーオプションの 1 つを使用するように構成します。詳細については、Spring リファレンスドキュメントを参照してください。次の例は、JAXB を使用してリポジトリポピュレータをアンマーシャルする方法を示しています。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:repository="http://www.springframework.org/schema/data/repository"
xmlns:oxm="http://www.springframework.org/schema/oxm"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/repository
https://www.springframework.org/schema/data/repository/spring-repository.xsd
http://www.springframework.org/schema/oxm
https://www.springframework.org/schema/oxm/spring-oxm.xsd">
<repository:unmarshaller-populator locations="classpath:data.json"
unmarshaller-ref="unmarshaller" />
<oxm:jaxb2-marshaller contextPath="com.acme" />
</beans>