---
title: Spring Data RESTのTips
tags: ["Spring Data REST", "Spring Boot", "Java"]
categories: ["Programming", "Java", "org", "springframework", "data", "rest"]
date: 2017-01-06T08:40:42Z
updated: 2017-01-07T03:29:30Z
---

[Spring Data REST](http://projects.spring.io/spring-data-rest/)はSpring Dataの`Repository`を作ればREST APIとして公開してくれる便利プロジェクト。自分は好きで、RESTサービスを作る時は基本これを使う。

Spring Data RESTを使うと、RESTアプリケーションの作りは、典型的な

* Controller -> Service -> Repository

ではなく

* Repository -> RepositoryEventHandler

になる。

Tipsというか、いつも設定している内容をメモっておく。

### サーバーサイド

#### リソースのURLにprefixをつける

デフォルトでは`<Host Name>/<Resource Name(複数形)>`になるが、Spring Data RESTが提供するAPIのパスにはprefixをつけた方が認可設定とかしやすい。

例えば、`<Host Name>/v1/<Resource Name(複数形)>`にする場合は、`application.properties`に

``` properties
spring.data.rest.base-path=/v1
```

を設定する。

#### Bean Validationを有効にする

依存関係の順番で`@Lazy`つけておかないとエラーになる。`Validator`が複数登録されることもあるので、`@Qualifier("mvcValidator")`もあると無難。

``` java
	@Configuration
	public static class RestConfig extends RepositoryRestConfigurerAdapter {
		private final Validator validator;

		public RestConfig(@Lazy @Qualifier("mvcValidator") Validator validator) {
			this.validator = validator;
		}

		@Override
		public void configureValidatingRepositoryEventListener(
				ValidatingRepositoryEventListener validatingListener) {
			validatingListener.addValidator("beforeCreate", validator);
			validatingListener.addValidator("beforeSave", validator);
		}

		/* ... */
	}
```

#### EntityのIDをレスポンスに含める

デフォルトだと、リソースのレスポンスにIDフィールドが含まれず、`_links`にエンティにアクセスするためのURLが入る。
これでもいいが、ID入っている方が便利な時があるので、その時は明示的にexposeする設定を書く。

``` java
	@Configuration
	public static class RestConfig extends RepositoryRestConfigurerAdapter {
		/* ... */

		@Override
		public void configureRepositoryRestConfiguration(
				RepositoryRestConfiguration config) {
			config.exposeIdsFor(FooBar.class);
		}
	}
```

### 一部のAPIだけRESTで公開する

デフォルトだと`Repository`のCRUD全APIが公開される。

公開して欲しくないAPIを除く場合はメソッドをOverrideして`@RestResource(exported = false)`をつける。

``` java
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RestResource;

public interface FooBarRepository extends CrudRepository<FooBar, Long> {
	@RestResource(exported = false)
	FooBar save(FooBar foobar);

	@RestResource(exported = false)
	void delete(Long id);
}
```

欲しいAPIだけ定義する場合は空の`Repository`から定義していく。

``` java
import java.util.Optional;
import java.util.UUID;

import org.springframework.data.repository.Repository;

public interface FooBarRepository extends Repository<FooBar, Long> {
	Optional<FooBar> findOne(Long id);

	FooBar save(FooBar foobar);

	void delete(Long id);
}
```

公開されているAPIは`http://localhost:8080/profile/foobars`で確認できる(`spring.data.rest.base-path=/v1`が付いている場合は`http://localhost:8080/v1/profile/foobars`)。


#### トランザクション管理

デフォルトでは`@RepositoryEventHandler`の処理と`Repository`の処理が別トランザクションになってしまう。作り上難しいのだが、同じトランザクション内で処理するのに、無理やりインターセプタを適用する。

``` java
	// Workarround http://stackoverflow.com/a/30713264/5861829
	@Configuration
	static class TxConfig {
		private final PlatformTransactionManager transactionManager;

		public TxConfig(PlatformTransactionManager transactionManager) {
			this.transactionManager = transactionManager;
		}

		@Bean
		TransactionInterceptor txAdvice() {
			NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
			source.addTransactionalMethod("post*", new DefaultTransactionAttribute());
			source.addTransactionalMethod("put*", new DefaultTransactionAttribute());
			source.addTransactionalMethod("delete*", new DefaultTransactionAttribute());
			source.addTransactionalMethod("patch*", new DefaultTransactionAttribute());
			return new TransactionInterceptor(transactionManager, source);
		}

		@Bean
		Advisor txAdvisor() {
			AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
			pointcut.setExpression(
					"execution(* org.springframework.data.rest.webmvc.RepositoryEntityController.*(..))");
			return new DefaultPointcutAdvisor(pointcut, txAdvice());
		}
	}
```

#### OAuth2のリソースサーバーにした時のスコープによる認可設定

例えばAccountリソース(パスは`/v1/accoounts`)に対してGETは`account.read`または`admin.read`スコープ、POST, PUT, PATCH, DELETEには`account.write`または`admin.write`スコープが必要にしたい場合。

``` java
	@Configuration
	@EnableResourceServer
	static class OAuth2ResourceConfig extends ResourceServerConfigurerAdapter {
		@Override
		public void configure(HttpSecurity http) throws Exception {
			http.sessionManagement()
					.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
					.antMatcher("/v1/**").authorizeRequests()
					.antMatchers(HttpMethod.GET, "/v1/accounts/**")
					.access("#oauth2.hasAnyScope('account.read', 'admin.read')")
					.antMatchers(HttpMethod.POST, "/v1/accounts/**")
					.access("#oauth2.hasAnyScope('account.write', 'admin.write')")
					.antMatchers(HttpMethod.PUT, "/v1/accounts/**")
					.access("#oauth2.hasAnyScope('account.write', 'admin.write')")
					.antMatchers(HttpMethod.PATCH, "/v1/accounts/**")
					.access("#oauth2.hasAnyScope('account.write', 'admin.write')")
					.antMatchers(HttpMethod.DELETE, "/v1/accounts/**")
					.access("#oauth2.hasAnyScope('account.write', 'admin.write')");
		}
	}
```

当然、OAuth2クライアントに対するスコープの設定は認可サーバー側の話。

### クライアントサイド

#### `Jackson2HalModule`を有効にする

サーバーサイドが`spring-data-rest`だと`Resources`型ででシリアライズしたい。デフォルトではでシリアライザが有効にならないので`ObjectMapper`に追加が必要。

``` java
	@Bean
	Jackson2ObjectMapperBuilderCustomizer objectMapperBuilderCustomizer() {
		return builder -> {
			builder.modulesToInstall(new Jackson2HalModule());
		};
	}
```

`RestTemplate`は`RestTemplateBuilder`で作る。

``` java
	@Bean
	RestTemplate restTemplate(RestTemplateBuilder builder) {
		return builder.build();
	}
```

`OAuth2RestTemplate`を使う場合は`MessageConverter`を明示的に設定しないといけない。

``` java
	@Bean
	OAuth2RestTemplate restTemplate(OAuth2ClientContext oauth2ClientContext,
			OAuth2ProtectedResourceDetails resource,
			HttpMessageConverters messageConverters, ObjectMapper objectMapper) {
		OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resource,
				oauth2ClientContext);
		// Set same message converters as RestTemplate to use Jackson2HalModule
		restTemplate.setMessageConverters(messageConverters.getConverters());
		// ...
		return restTemplate;
	}
```

ちなみにwebアプリ側で`@EnableOAuthSso`を使っている場合は、でトークン上記の認可設定をしている場合は、次の設定をして[`spring-cloud-starter-oauth2`](https://github.com/spring-cloud/spring-cloud-security)を依存ライブラリに追加すれば、`OAuth2RestTemplate`に自動でアクセストークンが入る。

``` properties
security.oauth2.client.client-id=<Client ID>
security.oauth2.client.client-secret=<Client Secret>
security.oauth2.client.access-token-uri=<Auth Server URL>/oauth/token
security.oauth2.client.user-authorization-uri=<Auth Server URL>/oauth/authorize
security.oauth2.client.scope=account.read,account.write
```

---

また追加する。
