REST APIを扱うSPAで遊びたくなったので、Spring Bootで超爆速&超軽量のREST APIを作ってみました。
環境
- Spring Boot: 4.0.3
- Spring Data JDBC: 4.0.3
- Spring Data REST: 5.0.3
- springdoc-openapi: 3.0.1
利用したライブラリについて
Spring Data JDBC
Spring Data JDBC は、Spring Data ファミリーの一部であり、JDBC をベースにしたシンプルで分かりやすいデータアクセスフレームワークです。
JPA(Java Persistence API)のような高度な ORM 機能(遅延ロード、キャッシュ、ダーティチェックなど)をあえて排除し、開発者が SQL の動作を完全に制御できるよう設計されています。
主な特徴とメリット
- シンプリシティ: JPA のような複雑なライフサイクル管理やキャッシュがなく、実行される SQL が予測しやすいのが最大の特徴です
- リポジトリの抽象化: CrudRepository を継承するだけで、基本的な CRUD 操作が自動で実装されます
DDD(ドメイン駆動設計)への適応: 「集約(Aggregate)」の概念に基づき、集約ルートを保存するとその配下のエンティティも一括で保存される仕組みを提供します。 - 不変エンティティのサポート: Java の record や不変オブジェクト(Immutable Object)との相性が良く、クリーンなドメインモデルを構築できます
- ネイティブ SQL の活用: @Query アノテーションを使用して、複雑なクエリを使い慣れた SQL で直接記述できます
| 機能 | Spring Data JDBC | Spring Data JPA |
|---|---|---|
| 遅延ロード (Lazy Load) | 非サポート(常に即時読み込み) | サポートあり |
| キャッシュ (L1/L2) | なし(常に DB へアクセス) | あり(パフォーマンス向上に寄与) |
| SQL 生成 | シンプルな SQL のみ自動生成 | 複雑な JOIN も含め自動生成 |
| クエリ言語 | 純粋な SQL | JPQL / クライテリア API |
| 学習コスト | 低い(JDBC/SQL の知識で十分) | 高い(JPA 固有の概念理解が必要) |
Spring Data REST
Spring Data REST は、Spring Data のリポジトリ定義を基に、RESTful な API エンドポイントを自動で生成・公開するライブラリです。
Spring Data JDBC などのデータアクセス層の上に構築され、コントローラーやサービス層のコードをほとんど書かずに、データベースを外部から操作可能な API として公開できます。
Spring Data REST は、単に「CRUD を自動化するツール」ではなく、「データモデルを、セマンティクス(意味論)を伴った完全な自己発見型 API (HATEOAS) として公開するエンジン」です。
最大の特徴は、「リンク(HAL)」でリソースを繋ぎ、「プロファイル(ALPS)」でリソースの意味を定義することで、クライアントが API の仕様書(ドキュメント)を読み込まなくても、API 自体からルールを学習できる仕組みを提供している点にあります。
主な特徴
- API の自動生成: CrudRepository などを継承したインターフェースを作成するだけで、CRUD(作成、読み取り、更新、削除)用のエンドポイントが即座に利用可能となります
- HATEOAS 対応: レスポンスに次の操作(リンク)を含める HAL(Hypertext Application Language) 形式をサポートしており、またそのデータが何を意味するかを含める ALPS (Application-Level Profile Semantics) をサポートしており、クライアントが次に何をすべきかを自己発見できる API を構築できます
- 高機能な標準機能: ページネーションとソート: クエリパラメータ(?page=0&size=10&sort=name,asc)で簡単に制御可能です
検索メソッドの公開: リポジトリに定義した findByName などのカスタムメソッドも、自動的に検索用エンドポイントとして公開されます。 - 柔軟なカスタマイズ: @RepositoryRestResource アノテーションを使って、公開するパスの変更や、特定メソッドの非公開設定などが可能です
構成要素の三位一体
Spring Data REST を使うと、以下の3つがセットで提供されます。
| 要素 | 役割 | メリット |
|---|---|---|
| Spring Data リポジトリ | データの永続化 | DB操作を抽象化 |
| HAL (JSON) | リソース間の接続 | クライアントがURLをハードコードしなくて済む |
| ALPS (Metadata) | セマンティクスの定義 | クライアントがデータの意味や型を動的に知ることができる |
なぜこれが重要なのか?
通常の API 開発では、サーバー側でエンドポイントを追加するたびに、フロントエンド側で URL を更新し、型定義(TypeScript 等)を手動で書き換える必要があります。
しかし、Spring Data REST + ALPS の構成では:
- サーバーが 「今の状態で可能な操作」 をリンクとして提示する
- サーバーが 「データの構造と意味」 を ALPS として公開する
- クライアントはそれらを読み取って、動的に UI やリクエストを構築する ことが可能になります
SpringDoc OpenAPI
SpringDoc OpenAPI は、Spring Boot アプリケーションから OpenAPI 3 仕様のドキュメント(API 仕様書)を自動生成 するためのライブラリです。
これまでお話しした Spring Data REST とも密接に関係しており、両者を組み合わせることで、自動生成された REST API の仕様をそのまま外部へドキュメントとして公開できます。
主な特徴とメリット
- 自動生成: ソースコードのアノテーションや構成を解析し、OpenAPI 形式の JSON や YAML を自動で作ります
- Swagger UI の同梱: API をブラウザ上で一覧・テスト実行できる Swagger UI が、デフォルトで含まれています
- 最新仕様への対応: 以前主流だった SpringFox は開発が停滞しており、現在 OpenAPI 3.0 や Spring Boot 4 に対応している SpringDoc が事実上の標準となっています
Spring Data REST(ALPS)との関係
Spring Data REST が ALPS を使って「アプリケーションレベルのセマンティクス(意味)」を表現するのに対し、SpringDoc OpenAPI は OpenAPI 仕様を使って「技術的なインターフェース(パス、型、パラメータ)」を表現します。
- ALPS: 主にプログラム(クライアント)が API の意味を動的に理解するために使う
- SpringDoc (OpenAPI): 主に開発者が API の構造を確認したり、コード生成ツール(OpenAPI Generator)でクライアントコードを生成したりするために使う
今回作るアプリについて
今回は、簡単な顧客管理をREST APIでできるよう、REST APIを公開します。
さらに、REST APIを簡単に動作・テストできるよう、Swagger APIを公開します。
プロジェクトのセットアップ
Spring Initializr
Spring InitializrでDependenciesに以下を含めてプロジェクトを作成する。
- Spring Data JDBC
- Rest Repositories ※これがSpring Data RESTです
- SpringDoc OpenAPI
- H2 Database
- Lombok
今回は簡単なテスト用なので、H2データベースを利用します。
依存関係の追加
Spring Initializrでは、SpringDoc OpenAPIを利用するための参照が不足しているので、手動で追加します。
<dependencies>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>3.0.1</version>
</dependency>
<!-- 追加 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
SpringDoc OpenAPIはHATEOASに依存しているのですが、Spring Initializrでは追加してくれないため、自分で追加する必要があります。これはSpring Boot 4がより細かくモジュール化したことを吸収しきれていないバグであり、Spring Boot 3で実装する場合は必要ありません。
データベース接続の設定
アプリを実装していく前に、データベース接続を設定します。
spring:
application:
name: demo
datasource: # 追加
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
さらに、今回使用するテーブルとデータもセットアップします。
DROP TABLE IF EXISTS customer;
CREATE TABLE customer (
email VARCHAR(255) PRIMARY KEY,
password VARCHAR(255),
name VARCHAR(255),
registration_date DATE,
birth_date DATE,
phone_number VARCHAR(20),
address VARCHAR(255),
role VARCHAR(20) NOT NULL DEFAULT 'USER'
);
INSERT INTO customer (email, password, name, registration_date, birth_date, phone_number, address, role) VALUES
('john.doe@example.com', 'password', 'John Doe', '2023-01-01', '1990-05-15', '123-456-7890', '123 Main St', 'USER'),
('jane.doe@example.com', 'password', 'Jane Doe', '2023-02-01', '2000-08-20', '987-654-3210', '456 Elm St', 'USER'),
('alice.smith@example.com', 'password', 'Alice Smith', '2023-03-01', '2010-12-10', '555-123-4567', '789 Oak St', 'USER'),
アプリの実装
いよいよアプリの実装に入ります。
アプリでは、Spring Data JDBCで利用するエンティティとリポジトリだけ作ります。以下は作る必要がありません。
- サービス(
@Service) - コントローラ(
@RestController) - Swagger UIの画面
- HATEOAS 対応の部品
エンティティの実装
@Data // (1)
public class Customer {
@Id // (2)
private String email;
private String password;
private String name;
private LocalDate registrationDate;
private LocalDate birthDate;
private String phoneNumber;
private String address;
/**
* ユーザーロール(USER, ADMIN)
*/
private Role role;
public static enum Role {
USER, ADMIN
}
}
| 項番 | 説明 |
|---|---|
| (1) | LombokでGetter、Setterなどを自動生成します。 |
| (2) | テーブルの主キーとなる項目にSpring Dataの@Idを付与します。 |
リポジトリの実装
// (1)
public interface CustomerRepository extends CrudRepository<Customer, String> {
}
| 項番 | 説明 |
|---|---|
| (1) | Spring DataのCrudRepositoryを継承したリポジトリインターフェイスを作ります。CrudRepositoryの第1引数はエンティティ型、第2引数は@Idフィールド型です。 |
クライアントアプリにREST APIを許可する(CORS)
@Configuration // (1)
public class RestConfig {
@Bean // (2)
public RepositoryRestConfigurer repositoryRestConfigurer() {
return new RepositoryRestConfigurer() {
@Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
// (3)
cors.addMapping("/**").allowedOrigins("*").allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS");
}
};
}
}
| 項番 | 説明 |
|---|---|
| (1) |
@Configurationを付与し、Springの設定ファイルを作ります。 |
| (2) |
RepositoryRestConfigurerの@Beanで、Spring Data RESTが自動生成するREST APIの設定を変更できます。 |
| (3) | すべてのリクエストに対してCORS(オリジン間リソース共有)を許可します。 |
今回は簡単なテスト用なので、すべてのリクエストに対してCORSを許可しています。実際のセキュリティ基準に沿って設定してください。
アプリの動作確認
アプリを起動して、Swagger UIからREST APIを動作確認します。
$ mvn clean spring-boot:run
http://localhost:8080/swagger-ui/index.html にアクセスしてみましょう。Swagger UIで以下のREST APIが公開されており、実行できることが分かります。
- customer-entity-controller
- GET:
/customers - POST:
/customers - GET:
/customers/{id} - PUT:
/customers/{id} - DELETE:
/customers/{id} - PATCH:
/customers/{id}
- GET:
- profile-controller
- GET:
/profile - GET:
/profile/customers
- GET:
profile-controllerは、ALPSをサポートするために自動生成されます。これが嫌ならSpring Data RESTを利用するかどうか再検討しましょう。
次に、Swagger UIを介さずにREST APIが公開されていることを確認してみましょう。
curl -X 'GET' \
'http://localhost:8080/customers' \
-H 'accept: application/hal+json'
もしくはブラウザで http://localhost:8080/customers にアクセスすると、セットアップしたデータが返ってくるのが分かります。
以上でREST APIの公開は完了です。クライアントアプリを開発してREST APIを利用してみてください。
Tips
実はこれまでの実装では不都合を感じる部分がいくつかあります。ひとつづつ解決していきましょう。
GETで@Idフィールドを取得できない
Swagger UIでGETリクエストを実行してみると、エンティティで@Idフィールドにしていたemailが取得されません。(取得したオブジェクトは、オブジェクト単体を取得するGETへのリンクを有しており、リンク中にemailは含まれるので、情報が欠落しているわけではありません。)
これはHATEOASの原則として、REST APIに(データベースの内部的な)IDを含めない、あるいはIDの代わりにURIを用いるという考え方によるものです。
とはいえ、データベースのテーブル主キーとしてユーザに意味のあるIDを用いることはよくあることなので、データとして取得したい場合もあります。
これは、Spring Data RESTの設定を変えることで実現できます。
@Configuration
public class RestConfig {
@Bean
public RepositoryRestConfigurer repositoryRestConfigurer() {
return new RepositoryRestConfigurer() {
@Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
cors.addMapping("/**").allowedOrigins("*").allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS");
config.exposeIdsFor(Customer.class); // (1)
}
};
}
}
| 項番 | 説明 |
|---|---|
| (1) |
exposeIdsFor(IDを露出させる)に、対象のエンティティを含めます。エンティティは配列で複数指定できます。 |
エンティティは個別に指定する必要があり、リフレクションを駆使するなどしない限り自動的にIDを露出することはできません。
POSTでINSERT文ではなくUPDATE文が実行されてしまう
Spring Data JDBCはSpring DataのCrudRepositoryをベースにしているので、CrudRepository#saveメソッドがINSERT文とUPDATE文を担当しており、以下の条件によりどちらを実行するか切り分けています。
- エンティティの
@Idフィールドがnullの場合はINSERT - エンティティが
Persistableインターフェイスを実装している場合、Persistable#isNew=trueの場合はINSERT
Spring Data JDBCにおけるID生成(採番)は、JPAとは異なり「データベース側に任せる」のがデフォルトです。MySQLのAUTO_INCREMENTやPostgreSQLのSERIALなど、DB側で自動生成される仕組みを利用している場合は、1番目の条件を利用できます。
そうではなく、今回の例のように@Idフィールドがユーザに意味のある項目の場合、2番目の条件を満たす必要があります。
Spring Data JDBCは、Spring Data JPAのように「一度取得したデータ(キャッシュ)」を持たないステートレスな設計となっています。このため、次に説明するようにPersistableインターフェイスを実装し、エンティティ自体が状態を保持する必要があります。
Persistableインターフェイスを実装するよう、エンティティを実装しなおしましょう。
今回は、なるべくエンティティがPOJOに近いままとなるよう、汎化して実装します。
共通部品の抽象エンティティクラス
// (1)
public abstract class AbstractPersistableEntity<ID> implements Persistable<ID> {
@Transient // (2)
private boolean isNew = true;
@Override
@JsonIgnore // (3)
public boolean isNew() {
return isNew;
}
// (4)
public void markNotNew() {
this.isNew = false;
}
}
| 項番 | 説明 |
|---|---|
| (1) |
Persistableインターフェイスを実装します。ジェネリクスには@Idフィールド型を設定しますが、っこでは実装クラスに任せるためジェネリクスのまま委ねています。 |
| (2) | INSERT対象か判別するためのisNewフィールドとGetterを実装します。isNewフィールドはデータベースに永続化したくないので、@Transientを付与します。 |
| (3) |
newプロパティをREST APIの返却値に含めたくないので、Getterに@JsonIgnoreを付与します。 |
| (4) | エンティティをUPDATE対象とする場合にisNewフィールドをfalseにするメソッドを実装します。これは次の実装で使用します。 |
共通部品のSpring Data JDBCコールバック
@Configuration // (1)
public class DataJdbcConfig {
@Bean // (2)
public AfterConvertCallback<AbstractPersistableEntity<?>> afterConvertCallback() {
return entity -> {
entity.markNotNew(); // (3)
return entity;
};
}
}
| 項番 | 説明 |
|---|---|
| (1) |
@Configurationを付与し、Springの設定ファイルを作ります。 |
| (2) |
AfterConvertCallbackの@Beanで、Spring Data JDBCのエンティティがデータベースから取得された後に実行されるコールバックを設定できます。 |
| (3) | エンティティのisNewフィールドをfalseにします。 |
Spring Data RESTのPUT・DELETE・PATCHでは一度SELECTを実行して、更新対象のエンティティの現在の状態を確認します。このときにAfterConvertCallbackは実行され、エンティティがUPDATE対象であると認識できるようになります。
個別のエンティティ
@Data
@EqualsAndHashCode(callSuper = false) // (1)
public class Customer extends AbstractPersistableEntity<String> { // (2)
@Id
private String email;
private String password;
private String name;
private LocalDate registrationDate;
private LocalDate birthDate;
private String phoneNumber;
private String address;
/**
* ユーザーロール(USER, ADMIN)
*/
private Role role;
public static enum Role {
USER, ADMIN
}
// (2)
@Override
@Nullable
public String getId() {
return email;
}
}
| 項番 | 説明 |
|---|---|
| (1) |
@EqualsAndHashCode(callSuper = false)を付与し、親クラスのフィールド(isNew)をイコールとハッシュコードに含めないようにします。 |
| (2) | 作成したAbstractPersistableEntityを継承し、Persistable#getIdメソッドを実装します。 |
一部のリポジトリをREST APIとして公開したくない(もしくは手動開発のAPIを併用したい)
一部のテーブルをREST APIとして公開したくない場合や、一部のテーブルはビジネスロジックを含む手動開発のAPIとして公開したい場合もあると思います。
これは、Spring Data RESTの設定を変えることで実現できます。
spring:
application:
name: demo
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
data:
rest:
detection-strategy: annotated # (1)
| 項番 | 説明 |
|---|---|
| (1) |
spring.data.rest.detection-strategy=annotatedにして、アノテーションが付与されたリポジトリのみSpring Data RESTの対象とします。 |
@RepositoryRestResource // (1)
public interface CustomerRepository extends CrudRepository<Customer, String> {
@Override
@RestResource(exported = false) // (2)
void deleteById(Long id);
}
| 項番 | 説明 |
|---|---|
| (1) |
@RepositoryRestResourceを付与し、Spring Data RESTの対象であることを明示します。 |
| (2) |
@@RestResource(exported = false)を付与し、特定のメソッドをSpring Data RESTの対象外とすることもできます。 |
Spring Data RESTの自動生成を利用せず、ビジネスロジックを含む手動開発のAPIとして公開したい場合は、そのリポジトリに対してのみサービスとコントローラを作ればOKです。
リクエストの入力チェックをしたい
POSTやPUTリクエストを処理するREST APIでは、変なデータがデータベースに登録されては困ります。
これに対してSpring Data RESTでは、通常のSpring MVCのアプリケーションと同じように、入力値をバリデーションすることができます。
バリデーションの実装方法も通常と同様にBean Validationを適用するだけです。ただし、Spring Data RESTのデフォルトではバリデーションが無効なので、有効にする必要があります。
バリデーションの有効化
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<!-- 追加 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.15</version>
</dependency>
Spring Data RESTのバリデーションの有効化
@Configuration
@RequiredArgsConstructor
public class RestConfig {
private final Validator validator; // (1)
@Bean
public RepositoryRestConfigurer repositoryRestConfigurer() {
return new RepositoryRestConfigurer() {
@Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
cors.addMapping("/**").allowedOrigins("*").allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS");
}
// (2)
@Override
public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
validatingListener.addValidator("beforeCreate", validator);
validatingListener.addValidator("beforeSave", validator);
}
};
}
}
| 項番 | 説明 |
|---|---|
| (1) |
org.springframework.validation.Validatorをインジェクトします。ここではRequiredArgsConstructorでコンストラクタインジェクションしています。 |
| (2) |
RepositoryRestConfigurer#configureValidatingRepositoryEventListenerでリスナにValidatorを登録します。 |
通常はbeforeCreate(POSTリクエスト)、beforeSave(PUT・PATCHリクエスト)でバリデーションを有効化すれば良いです。特殊な場合はbeforeDelete(DELETEリクエスト)でも有効化する必要があるかもしれません。
また、Spring Data RESTは「リソース(エンティティ)はあるべき正しい姿(State)を常に維持すべきである」というRESTの思想に基づきバリデーショングループをサポートしていません。リクエストの種類ごとにバリデーションルールを変更したい場合は実装を工夫する必要があります。
バリデーションルールの定義
@Data
public class Customer {
@Id
@NotBlank // (1)
private String email;
@NotBlank
private String password;
@NotBlank
private String name;
@NotNull
private LocalDate registrationDate;
@NotNull
private LocalDate birthDate;
@NotBlank
private String phoneNumber;
@NotBlank
private String address;
/**
* ユーザーロール(USER, ADMIN)
*/
@NotNull
private Role role;
public static enum Role {
USER, ADMIN
}
}
| 項番 | 説明 |
|---|---|
| (1) | 各フィールドやクラスレベルに、Bean Validationのアノテーションを付与します。 |
アプリを起動して、Swagger UIからREST APIを動作確認します。
$ mvn clean spring-boot:run
http://localhost:8080/swagger-ui/index.html にアクセスしてみましょう。
- customer-entity-controller
- POST:
/customers
- POST:
入力項目をいくつか空にして実行すると、以下のように400エラーが返却されます。
{
"errors": [
{
"entity": "Customer",
"property": "password",
"invalidValue": "",
"message": "空白は許可されていません"
},
{
"entity": "Customer",
"property": "email",
"invalidValue": "",
"message": "空白は許可されていません"
}
]
}
ALPSのメタデータを公開したくない
これを言い出すとSpring Data RESTを使わなくて良いんじゃないか説もありますが、ALPSのメタデータは使わないので公開したくないという場合もあります。
これは、Spring Data RESTの設定を変えることで実現できます。
@Configuration
public class RestConfig {
@Bean
public RepositoryRestConfigurer repositoryRestConfigurer() {
return new RepositoryRestConfigurer() {
@Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
cors.addMapping("/**").allowedOrigins("*").allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS");
config.getMetadataConfiguration().setAlpsEnabled(false); // (1)
}
};
}
}
| 項番 | 説明 |
|---|---|
| (1) |
setAlpsEnabled(ALPSを有効化する)で、ALPSを無効にします。 |
http://localhost:8080/profile/customers にアクセスすると、404 Notfound が返却されるようになり、メタデータを参照できなくなります。
ただし、profile-controllerが無効になるわけではなく、/profileパスは有効なままだったり、Swagger UIにもパスが残ったままだったりと不完全な対応状況です。これが嫌ならSpring Data RESTを利用するかどうか再検討しましょう。
まとめ
今回は、Spring Data JDBC + Spring Data REST + springdoc-openapiで超爆速&超軽量のREST APIを作ってみました。
REST API公開までのコード量は超微量なので超軽量は達成できましたが、ちょっと苦しんだところもあり、超爆速ならSpring Data JDBCではなくSpring Data JPAのほうが好まれるかもしれません。ですが、導入で書いたようにシンプルなREST APIの公開という目的に対してSpring Data JPAは重厚すぎる(無駄になる機能が多すぎる)ので、Spring Data JDBCは技術的に妥当な選択肢かなと思います。