このチュートリアルでは、Spring Data REST とその強力なバックエンド機能を使用するアプリのコレクションと、React の洗練された機能を組み合わせて、わかりやすい UI を構築します。

  • Spring Data REST (英語) は、ハイパーメディアを使用したリポジトリをすばやく構築する方法を提供します。

  • React (英語) は、JavaScript での効率的で高速で使いやすいビューに対する Facebook のソリューションです。

パート 1 — 基本的な機能

Spring コミュニティへようこそ。

このセクションでは、最低限の Spring Data REST アプリケーションを迅速に起動して実行する方法を示します。次に、Facebook の React.js ツールセットを使用して、その上にシンプルな UI を構築する方法を示します。

ステップ 0 — 環境のセットアップ

このリポジトリからコード (GitHub) を自由に入手して、フォローしてください。

自分でやりたい場合は、https://start.spring.io (英語) にアクセスして、次の依存関係を選択してください。

  • Rest リポジトリ

  • Thymeleaf

  • JPA

  • H2

このデモでは、Java 8、Maven プロジェクト、および Spring Boot の最新の安定版リリースを使用しています。また、ES6 (英語) でコーディングされた React.js も使用します。これにより、クリーンで空のプロジェクトが作成されます。そこから、このセクションに明示的に示されているさまざまなファイルを追加したり、前述のリポジトリから借用したりできます。

はじめに ...

最初はデータがありました。そしてそれは良かった。しかしその後、人々はさまざまな手段でデータにアクセスしたいと考えました。何年もの間、人々は多くの MVC コントローラーを組み合わせ、多くは Spring の強力な REST サポートを使用していました。しかし、繰り返し行うには多くの時間がかかります。

Spring Data REST は、いくつかの仮定が行われた場合にこの問題がどれほど簡単になるかを示しています。

  • 開発者は、リポジトリモデルをサポートする Spring Data プロジェクトを使用します。

  • システムは、HTTP 動詞、標準化されたメディアタイプ、IANA 承認済みのリンク名 (英語) など、広く受け入れられている業界標準プロトコルを使用します。

ドメインを宣言する

ドメインオブジェクトは、Spring Data REST ベースのアプリケーションの基盤を形成します。このセクションでは、会社の従業員を追跡するアプリケーションを作成します。次のように、データ型を作成して開始します。

例 1: src/main/java/com/greglturnquist/payroll/Employee.java
@Entity (1)
public class Employee {

	private @Id @GeneratedValue Long id; (2)
	private String firstName;
	private String lastName;
	private String description;

	private Employee() {}

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

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;
		Employee employee = (Employee) o;
		return Objects.equals(id, employee.id) &&
			Objects.equals(firstName, employee.firstName) &&
			Objects.equals(lastName, employee.lastName) &&
			Objects.equals(description, employee.description);
	}

	@Override
	public int hashCode() {

		return Objects.hash(id, firstName, lastName, description);
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	@Override
	public String toString() {
		return "Employee{" +
			"id=" + id +
			", firstName='" + firstName + '\'' +
			", lastName='" + lastName + '\'' +
			", description='" + description + '\'' +
			'}';
	}
}
1@Entity は、リレーショナルテーブルに格納するためのクラス全体を示す JPA アノテーションです。
2@Id および @GeneratedValue は、主キーを記録するための JPA アノテーションであり、必要に応じて自動的に生成されます。

このエンティティは、従業員情報を追跡するために使用されます。この場合、名前と職務説明です。

Spring Data REST は JPA に限定されません。このチュートリアルでは表示されませんが、多くの NoSQL データストアをサポートしています。詳細については、REST で Neo4j データアクセスREST で JPA データアクセス、および REST で MongoDB データアクセスを参照してください。

リポジトリの定義

Spring Data REST アプリケーションのもう 1 つの重要な部分は、次のような対応するリポジトリ定義です。

例 2: src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
public interface EmployeeRepository extends CrudRepository<Employee, Long> { (1)

}
1 リポジトリは Spring Data Commons の CrudRepository を継承し、ドメインオブジェクトのタイプとそのプライマリキーをプラグインします。

必要なのはそれだけです!実際、最上位で表示されている場合は、インターフェースにアノテーションを付ける必要さえありません。IDE を使用して CrudRepository を開くと、事前定義されたメソッドのコレクションが見つかります。

必要に応じて、独自のリポジトリを定義できます。Spring Data REST も同様にサポートしています。

デモのプリロード

このアプリケーションを使用するには、次のようにデータを事前にロードする必要があります。

例 3: src/main/java/com/greglturnquist/payroll/DatabaseLoader.java
@Component (1)
public class DatabaseLoader implements CommandLineRunner { (2)

	private final EmployeeRepository repository;

	@Autowired (3)
	public DatabaseLoader(EmployeeRepository repository) {
		this.repository = repository;
	}

	@Override
	public void run(String... strings) throws Exception { (4)
		this.repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
	}
}
1 このクラスには Spring の @Component アノテーションが付いているため、@SpringBootApplication によって自動的に取得されます。
2Spring Boot の CommandLineRunner を実装するため、すべての Bean が作成および登録された後に実行されます。
3 コンストラクター注入とオートワイヤーを使用して、Spring Data の自動作成された EmployeeRepository を取得します。
4run() メソッドはコマンドライン引数で呼び出され、データをロードします。

Spring Data の最大かつ最も強力な機能の 1 つは、JPA クエリを作成できることです。これにより、開発時間が短縮されるだけでなく、バグやエラーのリスクも軽減されます。Spring Data は、リポジトリクラス内のメソッドの名前を調べ、保存、削除、検索など、必要な操作を見つけ出します。

これが、空のインターフェースを作成し、既に構築されている保存、検索、および削除操作を継承する方法です。

ルート URI の調整

デフォルトでは、Spring Data REST は / でリンクのルートコレクションをホストします。そのパスで Web UI をホストするため、次のようにルート URI を変更する必要があります。

例 4: src/main/resources/application.properties
spring.data.rest.base-path=/api

バックエンドの起動

完全に機能する REST API を作成するために必要な最後のステップは、次のように Spring Boot を使用して public static void main メソッドを記述することです。

例 5: src/main/java/com/greglturnquist/payroll/ReactAndSpringDataRestApplication.java
@SpringBootApplication
public class ReactAndSpringDataRestApplication {

	public static void main(String[] args) {
		SpringApplication.run(ReactAndSpringDataRestApplication.class, args);
	}
}

前のクラスと Maven ビルドファイルが https://start.spring.io (英語) から生成されたと仮定すると、IDE 内でその main() メソッドを実行するか、コマンドラインで ./mvnw spring-boot:run を入力することで起動できます。(Windows ユーザーの場合は mvnw.bat)。

Spring Boot の最新情報がなく、その仕組みについては、ジョシュロングの紹介プレゼンテーション (英語) のいずれかを参照してください。それをやった?押して!

REST サービスのツアー

アプリケーションが実行されている状態で、cURL (英語) (または他の好み (英語) のツール)を使用して、コマンドラインで確認できます。次のコマンド(出力とともに表示)は、アプリケーション内のリンクを一覧表示します。

$ curl localhost:8080/api
{
  "_links" : {
    "employees" : {
      "href" : "http://localhost:8080/api/employees"
    },
    "profile" : {
      "href" : "http://localhost:8080/api/profile"
    }
  }
}

ルートノードに ping を実行すると、HAL 形式の JSON ドキュメント (英語) にラップされたリンクのコレクションが返されます。

  • _links は利用可能なリンクのコレクションです。

  • employees は、EmployeeRepository インターフェースによって定義された従業員オブジェクトの集約ルートを指します。

  • profile は IANA 標準の関係であり、サービス全体に関する検出可能なメタデータを指します。これについては後のセクションで説明します。

employees リンクをナビゲートすることにより、このサービスをさらに掘り下げることができます。次のコマンド(出力とともに表示)はこれを行います。

$ curl localhost:8080/api/employees
{
  "_embedded" : {
    "employees" : [ {
      "firstName" : "Frodo",
      "lastName" : "Baggins",
      "description" : "ring bearer",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/employees/1"
        }
      }
    } ]
  }
}

この段階では、従業員のコレクション全体を表示しています。

前に事前にロードしたデータとともに、self リンクを持つ _links 属性が含まれています。これは、その特定の従業員の正規リンクです。正規とは何ですか? 「文脈の自由」を意味します。例: /api/orders/1/processor を介して同じユーザーを取得でき、従業員は特定のオーダーの処理に関連付けられています。ここでは、他のエンティティとの関連はありません。

リンクは REST の重要な側面です。これらは、関連アイテムにナビゲートするためのパワーを提供します。これにより、変更が発生するたびに書き直さなくても、他の関係者が API をナビゲートできるようになります。クライアントの更新は、クライアントがリソースへのパスをハードコードする場合の一般的な問題です。リソースを再構築すると、コードに大きな混乱が生じる可能性があります。リンクが使用され、ナビゲーションルートが維持される場合、そのような調整を行うことは簡単かつ柔軟になります。

必要に応じて、その従業員を表示することもできます。次のコマンド(出力とともに表示)はこれを行います。

$ curl localhost:8080/api/employees/1
{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "description" : "ring bearer",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/api/employees/1"
    }
  }
}

ドメインオブジェクトしかないため、_embedded ラッパーは必要ないことを除いて、ここではほとんど変更はありません。

それはすべてうまくいきますが、おそらく新しいエントリを作成するのに苦労しています。次のコマンド(出力とともに表示)はこれを行います。

$ curl -X POST localhost:8080/api/employees -d "{\"firstName\": \"Bilbo\", \"lastName\": \"Baggins\", \"description\": \"burglar\"}" -H "Content-Type:application/json"
{
  "firstName" : "Bilbo",
  "lastName" : "Baggins",
  "description" : "burglar",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/api/employees/2"
    }
  }
}

この関連ガイドに示すように、PUTPATCH および DELETE も使用できます。ただし、今のところは、洗練された UI の構築に進みます。

カスタム UI コントローラーのセットアップ

Spring Boot を使用すると、カスタム Web ページを簡単に立ち上げることができます。まず、次のように Spring MVC コントローラーが必要です。

例 6: src/main/java/com/greglturnquist/payroll/HomeController.java
@Controller (1)
public class HomeController {

	@RequestMapping(value = "/") (2)
	public String index() {
		return "index"; (3)
	}

}
1@Controller は、このクラスを Spring MVC コントローラーとしてマークします。
2@RequestMapping は、/ ルートをサポートするために index() メソッドにフラグを立てます。
3 テンプレートの名前として index を返します。Spring Boot の自動構成ビューリゾルバーは、src/main/resources/templates/index.html にマッピングします。

HTML テンプレートの定義

Thymeleaf を使用していますが、その機能の多くは実際には使用しません。開始するには、次のようにインデックスページが必要です。

例 7: src/main/resources/templates/index.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head lang="en">
    <meta charset="UTF-8"/>
    <title>ReactJS + Spring Data REST</title>
    <link rel="stylesheet" href="/main.css" />
</head>
<body>

    <div id="react"></div>

    <script src="built/bundle.js"></script>

</body>
</html>

このテンプレートの重要な部分は、中央の <div id="react"></div> コンポーネントです。ここで、レンダリングされた出力をプラグインするように React に指示します。

また、その bundle.js ファイルがどこから来たのか疑問に思うかもしれません。構築方法は次のセクションで説明します。

このチュートリアルでは main.css を示していませんが、上にリンクされていることがわかります。CSS に関しては、Spring Boot は src/main/resources/static で見つかったものを自動的に提供します。そこに独自の main.css ファイルを配置します。私たちの焦点は CSS ではなく React と Spring Data REST であるため、チュートリアルには示されていません。

JavaScript モジュールのロード

このセクションには、JavaScript ビットを地面から取り出すための最低限の情報が含まれています。JavaScripts コマンドラインツールはすべてインストールできますが、インストールする必要はありません。少なくともまだインストールしていません。代わりに、pom.xml ビルドファイルに次を追加するだけです。

例 8: JavaScript ビットの作成に使用される frontend-maven-plugin 
<plugin>
	<groupId>com.github.eirslett</groupId>
	<artifactId>frontend-maven-plugin</artifactId>
</plugin>

この小さなプラグインは複数の手順を実行します。

  • install-node-and-npm コマンドは、node.js とそのパッケージ管理ツール npm を target フォルダーにインストールします。(これにより、バイナリがソース管理下に置かれなくなり、clean でクリーンアウトできるようになります)。

  • npm コマンドは、指定された引数(install)で npm バイナリを実行します。これにより、package.json で定義されたモジュールがインストールされます。

  • webpack コマンドは webpack バイナリを実行し、webpack.config.js に基づいてすべての JavaScript コードをコンパイルします。

これらの手順は順番に実行され、本質的に node.js をインストールし、JavaScript モジュールをダウンロードし、JS ビットを構築します。

どのモジュールがインストールされていますか? JavaScript 開発者は通常、npm を使用して、次のような package.json ファイルを作成します。

例 9: package.json
{
  "name": "spring-data-rest-and-reactjs",
  "version": "0.1.0",
  "description": "Demo of ReactJS + Spring Data REST",
  "repository": {
    "type": "git",
    "url": "[email protected] (英語)  :spring-guides/tut-react-and-spring-data-rest.git"
  },
  "keywords": [
    "rest",
    "hateoas",
    "spring",
    "data",
    "react"
  ],
  "author": "Greg L. Turnquist",
  "license": "Apache-2.0",
  "bugs": {
    "url": "https://github.com/spring-guides/tut-react-and-spring-data-rest/issues"
  },
  "homepage": "https://github.com/spring-guides/tut-react-and-spring-data-rest",
  "dependencies": {
    "react": "^16.5.2",
    "react-dom": "^16.5.2",
    "rest": "^1.3.1"
  },
  "scripts": {
    "watch": "webpack --watch -d --output ./target/classes/static/built/bundle.js"
  },
  "devDependencies": {
    "@babel/core": "^7.1.0",
    "@babel/preset-env": "^7.1.0",
    "@babel/preset-react": "^7.0.0",
    "babel-loader": "^8.0.2",
    "webpack": "^4.19.1",
    "webpack-cli": "^3.1.0"
  }
}

主な依存関係は次のとおりです。

  • react.js: このチュートリアルで使用されるツールキット

  • rest.js: REST 呼び出しを行うために使用される CujoJS ツールキット

  • webpack: JavaScript コンポーネントを単一のロード可能なバンドルにコンパイルするために使用されるツールキット

  • バベル : ES6 を使用して JavaScript コードを記述し、ES5 にコンパイルしてブラウザーで実行するには

後で使用する JavaScript コードをビルドするには、次のように webpack (英語) のビルドファイルを定義する必要があります。

例 10: webpack.config.js
var path = require('path');

module.exports = {
    entry: './src/main/js/app.js',
    devtool: 'sourcemaps',
    cache: true,
    mode: 'development',
    output: {
        path: __dirname,
        filename: './src/main/resources/static/built/bundle.js'
    },
    module: {
        rules: [
            {
                test: path.join(__dirname, '.'),
                exclude: /(node_modules)/,
                use: [{
                    loader: 'babel-loader',
                    options: {
                        presets: ["@babel/preset-env", "@babel/preset-react"]
                    }
                }]
            }
        ]
    }
};

この webpack 構成ファイル:

  • entry point を ./src/main/js/app.js として定義します。本質的に、app.js (まもなく作成するモジュール)は、JavaScript アプリケーションのことわざである public static void main() です。webpack は、最終バンドルがブラウザーによってロードされたときにを起動するを知るために、これを知っている必要があります。

  • sourcemaps を作成して、ブラウザーで JS コードをデバッグしているときに、元のソースコードにリンクバックできるようにします。

  • すべての JavaScript ビットを ./src/main/resources/static/built/bundle.js にコンパイルします。これは、Spring Boot uber JAR と同等の JavaScript です。すべてのカスタムコードと require() 呼び出しによってプルされたモジュールは、このファイルに詰め込まれます。

  • ES6 React コードを標準ブラウザで実行できる形式にコンパイルするために、es2015 と react の両方のプリセットを使用して、Babel エンジンに接続します。

これらの各 JavaScript ツールの動作方法の詳細については、対応するリファレンスドキュメントを参照してください。

JavaScript の変更を自動的に確認したいですか? npm run-script watch を実行して、webpack を監視モードにします。ソースを編集すると、bundle.js が再生成されます。

これらすべての準備が整ったら、React ビットに焦点を合わせることができます。React ビットは、DOM のロード後に取得されます。次のように、パーツに分割されます。

webpack を使用して物を組み立てているため、先に進み、必要なモジュールをフェッチします。

例 11: src/main/js/app.js
const React = require('react'); (1)
const ReactDOM = require('react-dom'); (2)
const client = require('./client'); (3)
1React は、このアプリのビルドに使用される Facebook のメインライブラリの 1 つです。
2ReactDOM は、React の DOM およびサーバーレンダラーへのエントリポイントとして機能するパッケージです。汎用の React パッケージと組み合わせることを目的としています。
3client は、rest.js を構成して、HAL、URI テンプレートなどのサポートを含めるカスタムコードです。また、デフォルトの Accept リクエストヘッダーを application/hal+json に設定します。ここでコードを読むことができます (GitHub)
REST 呼び出しに使用するものは重要ではないため、client のコードは表示されません。ソースを自由に確認してください。しかし、ポイントは、Restangular(または好きなもの)をプラグインすることができ、概念はまだ適用されます。

React に飛び込む

React は、コンポーネントの定義に基づいています。多くの場合、1 つのコンポーネントは、親子関連で別のコンポーネントの複数のインスタンスを保持できます。この概念は、複数のレイヤーに拡張できます。

まず最初に、すべてのコンポーネントにトップレベルのコンテナーを用意しておくと非常に便利です。(これは、このシリーズ全体でコードを展開するにつれて明らかになります)現在、従業員リストのみがあります。ただし、後で他の関連コンポーネントが必要になる場合があるため、次から始めてください。

例 12: src/main/js/app.js - アプリコンポーネント
class App extends React.Component { (1)

	constructor(props) {
		super(props);
		this.state = {employees: []};
	}

	componentDidMount() { (2)
		client({method: 'GET', path: '/api/employees'}).done(response => {
			this.setState({employees: response.entity._embedded.employees});
		});
	}

	render() { (3)
		return (
			<EmployeeList employees={this.state.employees}/>
		)
	}
}
1class App extends React.Component{ …​ } は、React コンポーネントを作成するメソッドです。
2componentDidMount は、React が DOM のコンポーネントをレンダリングした後に呼び出される API です。
3render は、画面上にコンポーネントを「描画」する API です。
React では、大文字はコンポーネントの命名規則です。

App コンポーネントでは、Spring Data REST バックエンドから従業員の配列が取得され、このコンポーネントの state データに保存されます。

React コンポーネントには、stateproperties の 2 種類のデータがあります。

State は、コンポーネントがそれ自体を処理することが期待されるデータです。また、変動して変化する可能性のあるデータです。状態を読み取るには、this.state を使用します。更新するには、this.setState() を使用します。this.setState() が呼び出されるたびに、React は状態を更新し、前の状態と新しい状態の間の差分を計算し、ページ上の DOM に一連の変更を挿入します。これにより、UI が迅速かつ効率的に更新されます。

一般的な規則は、コンストラクターですべての属性を空にして状態を初期化することです。次に、componentDidMount を使用して属性を設定することにより、サーバーからデータを検索します。それ以降は、ユーザーアクションまたはその他のイベントによって更新を実行できます。

Properties には、コンポーネントに渡されるデータが含まれます。プロパティは変更されませんが、代わりに固定値です。これらを設定するには、すぐにわかるように、新しいコンポーネントを作成するときに属性に割り当てます。

JavaScript は、他の言語のようにデータ構造をロックダウンしません。値を割り当ててプロパティを破壊しようとすることはできますが、これは React の差動エンジンでは機能しないため、避ける必要があります。

このコードでは、関数は rest.js の約束に準拠 (英語) インスタンスである client を介してデータをロードします。/api/employees からの取得が完了すると、done() 内の関数を呼び出し、その HAL ドキュメント(response.entity._embedded.employees)に基づいて状態を設定します。に curl /api/employees の構造を見て、それがこの構造にどのようにマッピングされるかを参照してください。

状態が更新されると、フレームワークによって render() 関数が呼び出されます。従業員の状態データは、<EmployeeList /> React コンポーネントの作成に入力パラメーターとして含まれています。

次のリストは、EmployeeList の定義を示しています。

例 13: src/main/js/app.js - EmployeeList コンポーネント
class EmployeeList extends React.Component{
	render() {
		const employees = this.props.employees.map(employee =>
			<Employee key={employee._links.self.href} employee={employee}/>
		);
		return (
			<table>
				<tbody>
					<tr>
						<th>First Name</th>
						<th>Last Name</th>
						<th>Description</th>
					</tr>
					{employees}
				</tbody>
			</table>
		)
	}
}

JavaScript のマップ機能を使用して、this.props.employees は従業員レコードの配列から <Element /> React コンポーネントの配列に変換されます(後ほど説明します)。

次のリストを考慮してください。

<Employee key={employee._links.self.href} data={employee} />

上記のリストは、keydata の 2 つのプロパティを持つ新しい React コンポーネント(大文字形式に注意)を作成します。これらには、employee._links.self.href および employee から値が提供されます。

Spring Data REST を使用する場合は常に、self リンクが特定のリソースのキーになります。React は子ノードの一意の識別子を必要とし、_links.self.href は完璧です。

最後に、次のように、マッピングを使用して構築された employees の配列をラップした HTML テーブルを返します。

<table>
    <tr>
        <th>First Name</th>
        <th>Last Name</th>
        <th>Description</th>
    </tr>
    {employees}
</table>

状態、プロパティ、および HTML のこのシンプルなレイアウトは、React を使用して、シンプルでわかりやすいコンポーネントを宣言的に作成する方法を示しています。

このコードには HTML JavaScript の両方が含まれているか判定します。はい、これは JSX (英語) です。使用する必要はありません。React は純粋な JavaScript を使用して作成できますが、JSX 構文は非常に簡潔です。Babel.js の迅速な作業のおかげで、トランスパイラーは JSX と ES6 の両方のサポートを一度に提供します。

JSX には、ES6 (英語) の断片も含まれています。このコードで使用されているものは、アロー関数 (英語) です。独自のスコープ this でネストされた function() を作成することを避け、self 変数 (英語) を必要としないようにします。

ロジックを構造に混ぜることが心配ですか? React の API は、状態とプロパティを組み合わせた優れた宣言型構造を推奨します。React は、関連のない JavaScript と HTML の束を混合する代わりに、関連する状態とプロパティの小さなビットを備えた単純なコンポーネントを構築することを推奨します。単一のコンポーネントを見て、設計を理解できます。その後、組み合わせてより大きな構造を簡単に作成できます。

次に、<Employee /> が何であるかを次のように実際に定義する必要があります。

例 14: src/main/js/app.js - 従業員コンポーネント
class Employee extends React.Component{
	render() {
		return (
			<tr>
				<td>{this.props.employee.firstName}</td>
				<td>{this.props.employee.lastName}</td>
				<td>{this.props.employee.description}</td>
			</tr>
		)
	}
}

このコンポーネントは非常に単純です。従業員の 3 つのプロパティを囲む 1 つの HTML テーブル行があります。プロパティ自体は this.props.employee です。JavaScript オブジェクトを渡すと、サーバーから取得したデータを簡単に渡すことができることに注意してください。

このコンポーネントは状態を管理せず、ユーザー入力も処理しないため、他に行うことはありません。これは、上記の <EmployeeList /> に詰め込むように誘惑するかもしれません。それをしません!アプリを小さなコンポーネントに分割し、それぞれが 1 つのジョブを実行することで、将来機能を構築しやすくなります。

最後のステップは、次のように全体をレンダリングすることです。

例 15: src/main/js/app.js- レンダリングコード
ReactDOM.render(
	<App />,
	document.getElementById('react')
)

React.render() は 2 つの引数を受け入れます : 定義した React コンポーネントと、それを注入する DOM ノード。HTML ページで以前に <div id="react"></div> アイテムをどのように見たか覚えていますか?ここでピックアップしてプラグインします。

これらすべてを準備したら、アプリケーション(./mvnw spring-boot:run)を再実行して http://localhost:8080 にアクセスしてください。次のイメージは、更新されたアプリケーションを示しています。

basic 1

システムによってロードされた最初の従業員を見ることができます。

cURL を使用して新しいエントリを作成したことを覚えていますか?次のコマンドを使用して再度実行します。

curl -X POST localhost:8080/api/employees -d "{\"firstName\": \"Bilbo\", \"lastName\": \"Baggins\", \"description\": \"burglar\"}" -H "Content-Type:application/json"

ブラウザをリフレッシュすると、新しいエントリが表示されるはずです。

basic 2

これで、Web サイトに両方のリストが表示されます。

レビュー

本セクション :

  • ドメインオブジェクトと対応するリポジトリを定義しました。

  • Spring Data REST に本格的なハイパーメディアコントロールを使用してエクスポートさせます。

  • 親子関連で 2 つの単純な React コンポーネントを作成しました。

  • サーバーデータを取得し、単純な静的 HTML 構造としてレンダリングしました。

課題 ?

  • Web ページは動的ではありませんでした。新しいレコードを取得するには、ブラウザをリフレッシュする必要がありました。

  • Web ページは、ハイパーメディアコントロールまたはメタデータを使用しませんでした。代わりに、/api/employees からデータをフェッチするためにハードコードされました。

  • 読み取り専用です。cURL を使用してレコードを変更できますが、Web ページにはインタラクティブ機能はありません。

次のセクションでこれらの欠点に対処します。

パート 2 - ハイパーメディアコントロール

前のセクションでは、Spring Data REST を使用して従業員データを保存するバックエンド給与サービスを作成する方法を見つけました。欠落していた主な機能は、ハイパーメディアコントロールとリンクによるナビゲーションを使用することでした。代わりに、データを見つけるためにパスをハードコードしました。

このリポジトリからコード (GitHub) を自由に入手して、フォローしてください。このセクションは、前のセクションのアプリケーションに基づいており、追加のものが追加されています。

はじめに、データがありました ... そして REST がありました

HTTP ベースのインターフェースを REST API と呼んでいる人々の数にイライラしています。今日の例は、SocialSite REST API です。それが RPC です。RPC を叫びます ... ハイパーテキストが制約であるという概念で REST アーキテクチャスタイルを明確にするために何をする必要がありますか?つまり、アプリケーション状態のエンジン(および API)がハイパーテキストによって駆動されていない場合、RESTful にすることも REST API にすることもできません。限目。修正が必要な壊れたマニュアルがどこかにありますか?
— Roy T. フィールディング
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven

それでは、ハイパーメディアコントロール(つまり、ハイパーテキスト)とは正確に何であり、どのように使用できますか?調べるために、一歩後退して、REST のコアミッションを見ていきます。

REST の概念は、Web を成功に導いたアイデアを借用し、API に適用することでした。Web の広大なサイズ、動的な性質、およびクライアント(つまりブラウザ)の更新頻度が低いにもかかわらず、Web は驚くべき成功を収めています。Roy Fielding は、いくつかの制約と機能を使用して、API の生成と消費を同様に拡張できるかどうかを確認しようとしました。

制約の 1 つは、動詞の数を制限することです。REST の場合、主なものは GET、POST、PUT、DELETE、および PATCH です。他にもありますが、ここでは取り上げません。

  • GET: システムを変更せずにリソースの状態を取得する

  • POST: 場所を指定せに新しいリソースを作成する

  • PUT: 既存のリソースを置き換え、既に存在する他のもの(存在する場合)を上書きする

  • DELETE: 既存のリソースを削除する

  • PATCH: 既存のリソースを変更する (新しいリソースを作成するよりも部分的に)

これらは、よく知られた仕様を持つ標準化された HTTP 動詞です。既に作成された HTTP 操作を選択して使用することにより、新しい言語を発明したり、業界を教育したりする必要がなくなります。

REST のもう 1 つの制約は、メディアタイプを使用してデータの形式を定義することです。誰もが情報交換のために独自のダイアレクトを書く代わりに、いくつかのメディアタイプを開発するのが賢明でしょう。受け入れられる最も人気のあるものの 1 つは、メディアタイプ application/hal+json の HAL です。Spring Data REST のデフォルトのメディアタイプです。重要な点は、REST には単一のメディアタイプが集中化されていないことです。代わりに、人々はメディアの種類を開発し、接続して試してみることができます。さまざまなニーズが利用可能になると、業界は柔軟に移動できます。

REST の主要な機能は、関連リソースへのリンクを含めることです。例:オーダーを表示している場合、RESTful API には、関連する顧客へのリンク、アイテムのカタログへのリンク、およびおそらくオーダー元のストアへのリンクが含まれます。このセクションでは、ページングを紹介し、ナビゲーションページングリンクの使用方法も確認します。

バックエンドからページングをオンにする

フロントエンドハイパーメディアコントロールの使用を開始するには、追加のコントロールをオンにする必要があります。Spring Data REST はページングのサポートを提供します。これを使用するには、リポジトリ定義を次のように微調整します。

例 16: src/main/java/com/greglturnquist/payroll/EmployeeRepository.java
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {

}

インターフェースは PagingAndSortingRepository を継承し、ページサイズを設定するための追加オプションを追加し、ナビゲーションリンクを追加してページ間を移動します。バックエンドの残りは同じです(物事を面白くするためにいくつかの追加の事前ロードされたデータ (GitHub) を除いて)。

アプリケーション(./mvnw spring-boot:run)を再起動し、動作を確認します。次に、次のコマンド(出力とともに表示)を実行して、ページングの動作を確認します。

$ curl "localhost:8080/api/employees?size=2"
{
  "_links" : {
    "first" : {
      "href" : "http://localhost:8080/api/employees?page=0&size=2"
    },
    "self" : {
      "href" : "http://localhost:8080/api/employees"
    },
    "next" : {
      "href" : "http://localhost:8080/api/employees?page=1&size=2"
    },
    "last" : {
      "href" : "http://localhost:8080/api/employees?page=2&size=2"
    }
  },
  "_embedded" : {
    "employees" : [ {
      "firstName" : "Frodo",
      "lastName" : "Baggins",
      "description" : "ring bearer",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/employees/1"
        }
      }
    }, {
      "firstName" : "Bilbo",
      "lastName" : "Baggins",
      "description" : "burglar",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/employees/2"
        }
      }
    } ]
  },
  "page" : {
    "size" : 2,
    "totalElements" : 6,
    "totalPages" : 3,
    "number" : 0
  }
}

デフォルトのページサイズは 20 ですが、それほど多くのデータはありません。そのため、動作を確認するために、?size=2 を設定します。予想どおり、2 人の従業員のみがリストされています。さらに、firstnext および last リンクもあります。self リンクもあります。これは、ページパラメーターを含むコンテキストがありません。

next リンクに移動すると、prev リンクも表示されます。次のコマンド(出力とともに表示)はこれを行います。

$ curl "http://localhost:8080/api/employees?page=1&size=2"
{
  "_links" : {
    "first" : {
      "href" : "http://localhost:8080/api/employees?page=0&size=2"
    },
    "prev" : {
      "href" : "http://localhost:8080/api/employees?page=0&size=2"
    },
    "self" : {
      "href" : "http://localhost:8080/api/employees"
    },
    "next" : {
      "href" : "http://localhost:8080/api/employees?page=2&size=2"
    },
    "last" : {
      "href" : "http://localhost:8080/api/employees?page=2&size=2"
    }
  },
...
URL クエリパラメーターで & を使用する場合、コマンドラインはそれが改行であると見なします。この問題を回避するには、URL 全体を引用符で囲みます。

それはきれいに見えますが、それを利用するためにフロントエンドを更新するとさらに良くなります。

関連によるナビゲート

Spring Data REST がすぐに提供するハイパーメディアコントロールの使用を開始するために、バックエンドでこれ以上の変更は必要ありません。フロントエンドでの作業に切り替えることができます。(それは Spring Data REST の美しさの一部です:乱雑なコントローラーの更新はありません!)

このアプリケーションは「Spring Data REST 固有」ではないことを指摘することが重要です。代わりに、HAL (英語) URI テンプレート (英語) 、およびその他の標準を使用します。それが rest.js の使用が簡単な理由です。そのライブラリには HAL サポートが付属しています。

前のセクションでは、パスを /api/employees にハードコードしました。代わりに、ハードコードする必要がある唯一のパスは、次のようにルートです

...
var root = '/api';
...

便利な小さな follow() 関数 (GitHub) を使用して、次のようにルートから開始して目的の場所に移動できます。

componentDidMount() {
	this.loadFromServer(this.state.pageSize);
}

前のセクションでは、ロードは componentDidMount() 内で直接行われました。このセクションでは、ページサイズが更新されたときに従業員のリスト全体を再読み込みできるようにします。そのために、次のように loadFromServer() に物を移動しました。

loadFromServer(pageSize) {
	follow(client, root, [
		{rel: 'employees', params: {size: pageSize}}]
	).then(employeeCollection => {
		return client({
			method: 'GET',
			path: employeeCollection.entity._links.profile.href,
			headers: {'Accept': 'application/schema+json'}
		}).then(schema => {
			this.schema = schema.entity;
			return employeeCollection;
		});
	}).done(employeeCollection => {
		this.setState({
			employees: employeeCollection.entity._embedded.employees,
			attributes: Object.keys(this.schema.properties),
			pageSize: pageSize,
			links: employeeCollection.entity._links});
	});
}

loadFromServer は、前のセクションと非常によく似ています。ただし、follow() を使用します。

  • follow() 関数の最初の引数は、REST 呼び出しを行うために使用される client オブジェクトです。

  • 2 番目の引数は、開始するルート URI です。

  • 3 番目の引数は、ナビゲートする関連の配列です。各文字列またはオブジェクトを指定できます。

関連の配列は ["employees"] のように単純にすることができます。つまり、最初の呼び出しが行われたとき、employees という名前の関連(または rel)を _links で調べます。href を見つけてナビゲートします。配列に別の関連がある場合は、プロセスを繰り返します。

rel だけでは不十分な場合があります。このコードの断片では、?size=<pageSize> のクエリパラメーターもプラグインします。後で見るように、提供できる他のオプションがあります。

JSON スキーマメタデータの取得

サイズベースのクエリで employees に移動すると、employeeCollection が利用可能になります。前のセクションで、そのデータを <EmployeeList /> 内に表示しました。このセクションでは、/api/profile/employees/ で見つかった JSON スキーマメタデータ (英語) を取得するために別の呼び出しを実行しています。

次の curl コマンドを実行することにより、自分でデータを表示できます(出力とともに表示)。

$ curl http://localhost:8080/api/profile/employees -H "Accept:application/schema+json"
{
  "title" : "Employee",
  "properties" : {
    "firstName" : {
      "title" : "First name",
      "readOnly" : false,
      "type" : "string"
    },
    "lastName" : {
      "title" : "Last name",
      "readOnly" : false,
      "type" : "string"
    },
    "description" : {
      "title" : "Description",
      "readOnly" : false,
      "type" : "string"
    }
  },
  "definitions" : { },
  "type" : "object",
  "$schema" : "https://json-schema.org/draft-04/schema#"
}
/profile/employees のメタデータのデフォルト形式は ALPS (英語) です。ただし、この場合、コンテンツネゴシエーションを使用して JSON スキーマを取得しています。

この情報を `<App />` コンポーネントの状態でキャプチャーすることにより、後で入力フォームを構築するときにそれを有効に活用できます。

新しいレコードを作成する

このメタデータを装備して、UI にいくつかの追加コントロールを追加できるようになりました。次のように、新しい React コンポーネント <CreateDialog /> を作成することから開始できます。

class CreateDialog extends React.Component {

	constructor(props) {
		super(props);
		this.handleSubmit = this.handleSubmit.bind(this);
	}

	handleSubmit(e) {
		e.preventDefault();
		const newEmployee = {};
		this.props.attributes.forEach(attribute => {
			newEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
		});
		this.props.onCreate(newEmployee);

		// clear out the dialog's inputs
		this.props.attributes.forEach(attribute => {
			ReactDOM.findDOMNode(this.refs[attribute]).value = '';
		});

		// Navigate away from the dialog to hide it.
		window.location = "#";
	}

	render() {
		const inputs = this.props.attributes.map(attribute =>
			<p key={attribute}>
				<input type="text" placeholder={attribute} ref={attribute} className="field"/>
			</p>
		);

		return (
			<div>
				<a href="#createEmployee">Create</a>

				<div id="createEmployee" className="modalDialog">
					<div>
						<a href="#" title="Close" className="close">X</a>

						<h2>Create new employee</h2>

						<form>
							{inputs}
							<button onClick={this.handleSubmit}>Create</button>
						</form>
					</div>
				</div>
			</div>
		)
	}

}

この新しいコンポーネントには、handleSubmit() 機能と予想される render() 機能の両方があります。

これらの関数を逆順で掘り下げ、まず render() 関数を調べます。

レンダリング

コードは、attributes プロパティにある JSON スキーマデータをマッピングし、それを <p><input></p> 要素の配列に変換します。

  • key は、React が複数の子ノードを区別するために再び必要です。

  • これは、単純なテキストベースの入力フィールドです。

  • placeholder を使用すると、フィールドがどちらであるかをユーザーに表示できます。

  • name 属性を持つことに慣れているかもしれませんが、必須ではありません。React では、ref は特定の DOM ノードを取得するためのメカニズムです(すぐにわかるように)。

これは、サーバーからデータをロードすることにより駆動されるコンポーネントの動的な性質を表しています。

このコンポーネントのトップレベル <div> の中には、アンカータグと別の <div> があります。アンカータグは、ダイアログを開くためのボタンです。そして、ネストされた <div> は隠されたダイアログそのものです。この例では、純粋な HTML5 と CSS3 を使用しています。JavaScript はまったくありません!ダイアログの表示と非表示に使用される CSS コードを確認できます (GitHub) 。ここでは詳しく説明しません。

<div id="createEmployee"> の内部には、入力フィールドの動的リストが挿入され、その後に Create ボタンが挿入されるフォームがあります。そのボタンには onClick={this.handleSubmit} イベントハンドラーがあります。これは、イベントハンドラーを登録する React の方法です。

React は、すべての DOM 要素にイベントハンドラーを作成しません。代わりに、はるかに高性能で洗練された (英語) ソリューションがあります。そのインフラストラクチャを管理する必要はなく、代わりに機能コードの作成に集中できます。

ユーザー入力の処理

handleSubmit() 関数は、最初にイベントが階層をさらに上にバブルするのを停止します。次に、React.findDOMNode(this.refs[attribute]) を使用して、同じ JSON スキーマ属性プロパティを使用して各 <input> を検索します。

this.refs は、名前で特定の React コンポーネントに手を伸ばして取得する方法です。仮想 DOM コンポーネントのみを取得していることに注意してください。実際の DOM 要素を取得するには、React.findDOMNode() を使用する必要があります。

すべての入力を反復処理して newEmployee オブジェクトを作成した後、新しい従業員レコードの onCreate() へのコールバックを呼び出します。この関数は App.onCreate 内にあり、この React コンポーネントに別のプロパティとして提供されました。そのトップレベル関数がどのように動作するかを参照してください。

onCreate(newEmployee) {
	follow(client, root, ['employees']).then(employeeCollection => {
		return client({
			method: 'POST',
			path: employeeCollection.entity._links.self.href,
			entity: newEmployee,
			headers: {'Content-Type': 'application/json'}
		})
	}).then(response => {
		return follow(client, root, [
			{rel: 'employees', params: {'size': this.state.pageSize}}]);
	}).done(response => {
		if (typeof response.entity._links.last !== "undefined") {
			this.onNavigate(response.entity._links.last.href);
		} else {
			this.onNavigate(response.entity._links.self.href);
		}
	});
}

ここでも、follow() 関数を使用して、POST 操作が実行される employees リソースに移動します。この場合、パラメーターを適用する必要はなかったため、rel インスタンスの文字列ベースの配列は問題ありません。この状況では、POST 呼び出しが返されます。これにより、次の then() 句で POST の結果の処理を処理できます。

通常、新しいレコードはデータセットの最後に追加されます。特定のページを見ているため、新しい従業員レコードが現在のページにないことを期待するのは論理的です。これを処理するには、同じページサイズが適用されたデータの新しいバッチをフェッチする必要があります。その約束は done() 内の最終節に対して返されます。

ユーザーはおそらく新しく作成された従業員を見たいと思うため、ハイパーメディアコントロールを使用して last エントリに移動できます。

約束ベースの API を初めて使用しますか? 約束 (英語) は、非同期操作を開始し、タスクが完了したときに応答する関数を登録する方法です。約束は、「コールバック地獄」を回避するために一緒に連鎖されるように設計されています。次のフローを参照してください。

when.promise(async_func_call())
	.then(function(results) {
		/* process the outcome of async_func_call */
	})
	.then(function(more_results) {
		/* process the previous then() return value */
	})
	.done(function(yet_more) {
		/* process the previous then() and wrap things up */
	});

詳細については、promise に関するこのチュートリアル (英語) を参照してください。

promise で覚えておくべき秘密は、then() 関数値であろうと別の promise であろうと、何かを返す必要があるということです。done() 関数は何も返しません。また、チェーンは何も返しません。まだ気付いていない場合は、client (rest.js からの rest のインスタンス)と follow 関数が promise を返します。

データのページング

バックエンドでページングを設定し、新しい従業員を作成するときに既にそれを利用し始めています。

前のセクションでは、ページコントロールを使用して last ページにジャンプしました。UI に動的に適用し、ユーザーが必要に応じてナビゲートできるようにすることは非常に便利です。利用可能なナビゲーションリンクに基づいてコントロールを動的に調整することは素晴らしいことです。

まず、使用した onNavigate() 関数を確認しましょう。

onNavigate(navUri) {
	client({method: 'GET', path: navUri}).done(employeeCollection => {
		this.setState({
			employees: employeeCollection.entity._embedded.employees,
			attributes: this.state.attributes,
			pageSize: this.state.pageSize,
			links: employeeCollection.entity._links
		});
	});
}

これは、App.onNavigate 内の上部で定義されます。繰り返しますが、これは最上位コンポーネントの UI の状態を管理できるようにするためです。onNavigate() を <EmployeeList /> React コンポーネントに渡した後、次のハンドラーがコーディングされ、一部のボタンのクリックを処理します。

handleNavFirst(e){
	e.preventDefault();
	this.props.onNavigate(this.props.links.first.href);
}

handleNavPrev(e) {
	e.preventDefault();
	this.props.onNavigate(this.props.links.prev.href);
}

handleNavNext(e) {
	e.preventDefault();
	this.props.onNavigate(this.props.links.next.href);
}

handleNavLast(e) {
	e.preventDefault();
	this.props.onNavigate(this.props.links.last.href);
}

これらの各関数はデフォルトイベントをインターセプトし、バブリングを防ぎます。次に、適切なハイパーメディアリンクを使用して onNavigate() 関数を呼び出します。

これで、EmployeeList.render のハイパーメディアリンクに表示されるリンクに基づいて、条件付きでコントロールを表示できます。

render() {
	const employees = this.props.employees.map(employee =>
		<Employee key={employee._links.self.href} employee={employee} onDelete={this.props.onDelete}/>
	);

	const navLinks = [];
	if ("first" in this.props.links) {
		navLinks.push(<button key="first" onClick={this.handleNavFirst}>&lt;&lt;</button>);
	}
	if ("prev" in this.props.links) {
		navLinks.push(<button key="prev" onClick={this.handleNavPrev}>&lt;</button>);
	}
	if ("next" in this.props.links) {
		navLinks.push(<button key="next" onClick={this.handleNavNext}>&gt;</button>);
	}
	if ("last" in this.props.links) {
		navLinks.push(<button key="last" onClick={this.handleNavLast}>&gt;&gt;</button>);
	}

	return (
		<div>
			<input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/>
			<table>
				<tbody>
					<tr>
						<th>First Name</th>
						<th>Last Name</th>
						<th>Description</th>
						<th></th>
					</tr>
					{employees}
				</tbody>
			</table>
			<div>
				{navLinks}
			</div>
		</div>
	)
}

前のセクションと同様に、this.props.employees を <Element /> コンポーネントの配列に変換します。次に、navLinks の配列を HTML ボタンの配列として構築します。

React は XML に基づいているため、< を <button> 要素内に配置することはできません。代わりに、エンコードされたバージョンを使用する必要があります。

次に、返された HTML の下部に {navLinks} が挿入されているのを確認できます。

既存のレコードを削除する

エントリの削除ははるかに簡単です。必要なのは、HAL ベースのレコードを取得し、DELETE を self リンクに適用することだけです。

class Employee extends React.Component {

	constructor(props) {
		super(props);
		this.handleDelete = this.handleDelete.bind(this);
	}

	handleDelete() {
		this.props.onDelete(this.props.employee);
	}

	render() {
		return (
			<tr>
				<td>{this.props.employee.firstName}</td>
				<td>{this.props.employee.lastName}</td>
				<td>{this.props.employee.description}</td>
				<td>
					<button onClick={this.handleDelete}>Delete</button>
				</td>
			</tr>
		)
	}
}

Employee コンポーネントのこの更新されたバージョンでは、行の最後に追加のエントリ(削除ボタン)が表示されます。クリックすると、this.handleDelete を呼び出すように登録されます。handleDelete() 関数は、コンテキスト的に重要な this.props.employee レコードを提供しながら、渡されたコールバックを呼び出すことができます。

これも、1 つの場所で最上位コンポーネントの状態を管理するのが最も簡単であることを示しています。これは常にそうであるとは限りません。ただし、多くの場合、状態を 1 か所で管理すると、物事をまっすぐに簡単に保つことが容易になります。コンポーネント固有の詳細(this.props.onDelete(this.props.employee))でコールバックを呼び出すことにより、コンポーネント間の動作を非常に簡単に調整できます。

onDelete() 関数を App.onDelete の先頭にトレースすると、その動作を確認できます。

onDelete(employee) {
	client({method: 'DELETE', path: employee._links.self.href}).done(response => {
		this.loadFromServer(this.state.pageSize);
	});
}

ページベースの UI でレコードを削除した後に適用する動作は少し注意が必要です。この場合、サーバーからデータ全体をリロードし、同じページサイズを適用します。次に、最初のページが表示されます。

最後のページの最後のレコードを削除する場合、最初のページにジャンプします。

ページサイズの調整

ハイパーメディアが実際にどのように輝くかを確認する 1 つの方法は、ページサイズを更新することです。Spring Data REST は、ページサイズに基づいてナビゲーションリンクを流動的に更新します。

ElementList.render の上部には <input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/> という HTML 要素があります。

  • ref="pageSize" を使用すると、this.refs.pageSize を使用してその要素を簡単に取得できます。

  • defaultValue は、状態の pageSize で初期化します。

  • 以下に示すように、onInput はハンドラーを登録します。

    handleInput(e) {
    	e.preventDefault();
    	const pageSize = ReactDOM.findDOMNode(this.refs.pageSize).value;
    	if (/^[0-9]+$/.test(pageSize)) {
    		this.props.updatePageSize(pageSize);
    	} else {
    		ReactDOM.findDOMNode(this.refs.pageSize).value =
    			pageSize.substring(0, pageSize.length - 1);
    	}
    }

イベントの泡立ちを停止します。次に、<input> の ref 属性を使用して、すべて React の findDOMNode() ヘルパー関数を介して DOM ノードを見つけ、その値を抽出します。入力が実際に数字であるかどうかは、数字の文字列であるかどうかを確認してテストします。その場合、コールバックを呼び出して、新しいページサイズを App React コンポーネントに送信します。そうでない場合は、入力した文字が入力から取り除かれます。

App は updatePageSize() を取得するとどうなるでしょうか?見てみな:

updatePageSize(pageSize) {
	if (pageSize !== this.state.pageSize) {
		this.loadFromServer(pageSize);
	}
}

新しいページサイズの値により、すべてのナビゲーションリンクが変更されるため、データを再フェッチして最初から開始することをお勧めします。

すべてを一緒に入れて

これらすべての素晴らしい追加により、次のイメージに示すように、UI が大幅に改善されました。

hypermedia 1

ページサイズの設定は上部に、削除ボタンは各行に、ナビゲーションボタンは下部に表示されます。ナビゲーションボタンは、ハイパーメディアコントロールの強力な機能を示しています。

次のイメージでは、HTML 入力プレースホルダーにメタデータがプラグインされた CreateDialog を見ることができます。

hypermedia 2

これは、ドメイン駆動型メタデータ(JSON スキーマ)と組み合わせたハイパーメディアを使用する力を実際に示しています。Web ページは、どのフィールドがどのフィールドであるかを知る必要はありません。代わりに、ユーザーそれをて、それを使用する方法を知ることができますEmployee ドメインオブジェクトに別のフィールドを追加した場合、このポップアップは自動的にそれを表示します。

レビュー

本セクション :

  • Spring Data REST のページング機能を有効にしました。

  • ハードコードされた URI パスを捨て、関連名または「rels」と組み合わせたルート URI の使用を開始しました。

  • ページベースのハイパーメディアコントロールを動的に使用するように UI を更新しました。

  • 従業員を作成および削除し、必要に応じて UI を更新する機能を追加しました。

  • ページサイズを変更し、UI が柔軟に応答できるようにしました。

課題 ?

Web ページを動的にしました。ただし、別のブラウザタブを開いて、同じアプリをポイントします。1 つのタブを変更しても、他のタブは何も更新されません。

次のセクションでその課題に対処します。

パート 3 - 条件付き操作

前のセクションでは、Spring Data REST のハイパーメディアコントロールをオンにし、ページングによって UI をナビゲートし、ページサイズの変更に基づいて動的にサイズを変更する方法を見つけました。従業員を作成および削除し、ページを調整する機能を追加しました。ただし、現在編集している同じデータの一部に対して他のユーザーが行った更新を考慮しないと、ソリューションは完成しません。

このリポジトリからコード (GitHub) を自由に入手して、フォローしてください。このセクションは前のセクションに基づいていますが、追加機能が追加されています。

PUT するかしないか?それが質問です。

リソースをフェッチするとき、他の誰かがリソースを更新するとリソースが古くなる可能性があります。これに対処するために、Spring Data REST は、リソースのバージョン管理と ETag の 2 つのテクノロジーを統合します。

バックエンドのリソースをバージョン管理し、フロントエンドで ETag を使用することにより、条件付きで PUT を変更できます。つまり、リソースが変更されたかどうかを検出し、PUT (または PATCH)が他の誰かの更新を踏みにじることを防ぐことができます。

REST リソースのバージョン管理

リソースのバージョン管理をサポートするには、このタイプの保護を必要とするドメインオブジェクトのバージョン属性を定義します。次のリストは、Employee オブジェクトに対してこれを行うメソッドを示しています。

例 17: src/main/java/com/greglturnquist/payroll/Employee.java
@Entity
public class Employee {

	private @Id @GeneratedValue Long id;
	private String firstName;
	private String lastName;
	private String description;

	private @Version @JsonIgnore Long version;

	private Employee() {}

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

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;
		Employee employee = (Employee) o;
		return Objects.equals(id, employee.id) &&
			Objects.equals(firstName, employee.firstName) &&
			Objects.equals(lastName, employee.lastName) &&
			Objects.equals(description, employee.description) &&
			Objects.equals(version, employee.version);
	}

	@Override
	public int hashCode() {

		return Objects.hash(id, firstName, lastName, description, version);
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public Long getVersion() {
		return version;
	}

	public void setVersion(Long version) {
		this.version = version;
	}

	@Override
	public String toString() {
		return "Employee{" +
			"id=" + id +
			", firstName='" + firstName + '\'' +
			", lastName='" + lastName + '\'' +
			", description='" + description + '\'' +
			", version=" + version +
			'}';
	}
}
  • version フィールドには javax.persistence.Version のアノテーションが付けられています。行が挿入および更新されるたびに、値が自動的に保存および更新されます。

個々のリソース(コレクションリソースではない)をフェッチすると、Spring Data REST はこのフィールドの値を使用して ETag レスポンスヘッダー (英語) を自動的に追加します。

個々のリソースとそのヘッダーの取得

前のセクションでは、コレクションリソースを使用してデータを収集し、UI の HTML テーブルに入力しました。Spring Data REST では、_embedded データセットはデータのプレビューと見なされます。ETags のようなヘッダーを取得するには、データを確認するのに便利ですが、各リソースを個別に取得する必要があります。

このバージョンでは、loadFromServer が更新されてコレクションを取得します。次に、URI を使用して個々のリソースを取得できます。

例 18: src/main/js/app.js - 各リソースを取得する
loadFromServer(pageSize) {
	follow(client, root, [ (1)
		{rel: 'employees', params: {size: pageSize}}]
	).then(employeeCollection => { (2)
		return client({
			method: 'GET',
			path: employeeCollection.entity._links.profile.href,
			headers: {'Accept': 'application/schema+json'}
		}).then(schema => {
			this.schema = schema.entity;
			this.links = employeeCollection.entity._links;
			return employeeCollection;
		});
	}).then(employeeCollection => { (3)
		return employeeCollection.entity._embedded.employees.map(employee =>
				client({
					method: 'GET',
					path: employee._links.self.href
				})
		);
	}).then(employeePromises => { (4)
		return when.all(employeePromises);
	}).done(employees => { (5)
		this.setState({
			employees: employees,
			attributes: Object.keys(this.schema.properties),
			pageSize: pageSize,
			links: this.links
		});
	});
}
1follow() 関数は、employees コレクションリソースに移動します。
2 最初の then(employeeCollection ⇒ …​) 句は、JSON スキーマデータをフェッチする呼び出しを作成します。これには、メタデータとナビゲーションリンクを <App/> コンポーネントに保存するための内部 then 句があります。

この埋め込まれた promise は employeeCollection を返すことに注意してください。そうすれば、コレクションを次の呼び出しに渡すことができ、途中でメタデータを取得できます。

32 番目の then(employeeCollection ⇒ …​) 句は、従業員のコレクションを GET の配列に変換し、個々のリソースをフェッチすることを約束します。 This is what you need to fetch an ETag header for each employee
4then(employeePromises ⇒ …​) 句は GET 約束の配列を取り、when.all() で単一の約束にマージします。これは、すべての GET 約束が解決されると解決されます。
5loadFromServer は done(employees ⇒ …​) で終わり、このデータの融合を使用して UI 状態が更新されます。

このチェーンは、他の場所でも実装されています。例: onNavigate() (異なるページへのジャンプに使用)は、個々のリソースを取得するために更新されます。ここに示されているものとほぼ同じであるため、このセクションでは省略しています。

既存のリソースの更新

このセクションでは、UpdateDialog React コンポーネントを追加して、既存の従業員レコードを編集します。

例 19: src/main/js/app.js - UpdateDialog コンポーネント
class UpdateDialog extends React.Component {

	constructor(props) {
		super(props);
		this.handleSubmit = this.handleSubmit.bind(this);
	}

	handleSubmit(e) {
		e.preventDefault();
		const updatedEmployee = {};
		this.props.attributes.forEach(attribute => {
			updatedEmployee[attribute] = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
		});
		this.props.onUpdate(this.props.employee, updatedEmployee);
		window.location = "#";
	}

	render() {
		const inputs = this.props.attributes.map(attribute =>
			<p key={this.props.employee.entity[attribute]}>
				<input type="text" placeholder={attribute}
					   defaultValue={this.props.employee.entity[attribute]}
					   ref={attribute} className="field"/>
			</p>
		);

		const dialogId = "updateEmployee-" + this.props.employee.entity._links.self.href;

		return (
			<div key={this.props.employee.entity._links.self.href}>
				<a href={"#" + dialogId}>Update</a>
				<div id={dialogId} className="modalDialog">
					<div>
						<a href="#" title="Close" className="close">X</a>

						<h2>Update an employee</h2>

						<form>
							{inputs}
							<button onClick={this.handleSubmit}>Update</button>
						</form>
					</div>
				</div>
			</div>
		)
	}

};

この新しいコンポーネントには、<CreateDialog /> コンポーネントと同様に、handleSubmit() 機能と予想される render() 機能の両方があります。

これらの関数を逆の順序で掘り下げ、最初に render() 関数を調べます。

レンダリング

このコンポーネントは、前のセクションの <CreateDialog /> と同じダイアログを表示および非表示にするために、同じ CSS/HTML 戦術を使用します。

JSON スキーマ属性の配列を HTML 入力の配列に変換し、スタイル設定のために段落要素でラップします。これも <CreateDialog /> と同じですが、1 つの違いがあります。フィールドには this.props.employee がロードされます。CreateDialog コンポーネントでは、フィールドは空です。

id フィールドの構築方法は異なります。UI 全体に CreateDialog リンクは 1 つしかありませんが、表示される行ごとに個別の UpdateDialog リンクがあります。id フィールドは self リンクの URI に基づいています。これは、<div> 要素の React key、HTML アンカータグ、および非表示のポップアップで使用されます。

ユーザー入力の処理

送信ボタンは、コンポーネントの handleSubmit() 機能にリンクされています。これは、React refs (英語) を使用してポップアップの詳細を抽出するために、React.findDOMNode() を便利に使用します。

入力値が抽出されて updatedEmployee オブジェクトにロードされた後、最上位の onUpdate() メソッドが呼び出されます。これにより、React の片方向バインディングのスタイルが継続され、呼び出し対象の関数が上位コンポーネントから下位コンポーネントにプッシュされます。このように、状態は依然として上部で管理されます。

条件付き PUT

データモデルにバージョニングを組み込むために、このすべての努力を行ってきました。Spring Data REST は、その値を ETag レスポンスヘッダーとして提供しました。ここで、それを有効に活用できます。

例 20: src/main/js/app.js-onUpdate 関数
onUpdate(employee, updatedEmployee) {
	client({
		method: 'PUT',
		path: employee.entity._links.self.href,
		entity: updatedEmployee,
		headers: {
			'Content-Type': 'application/json',
			'If-Match': employee.headers.Etag
		}
	}).done(response => {
		this.loadFromServer(this.state.pageSize);
	}, response => {
		if (response.status.code === 412) {
			alert('DENIED: Unable to update ' +
				employee.entity._links.self.href + '. Your copy is stale.');
		}
	});
}

If-Match リクエストヘッダー (英語) を持つ PUT により、Spring Data REST は現在のバージョンに対して値をチェックします。受信 If-Match 値がデータストアのバージョン値と一致しない場合、Spring Data REST は HTTP 412 Precondition Failed で失敗します。

Promises/A+ (英語) の仕様では、実際に API を then(successFunction, errorFunction) として定義しています。これまでのところ、成功関数でのみ使用されるのを見てきました。上記のコードフラグメントには、2 つの関数があります。success 関数は loadFromServer を呼び出しますが、error 関数は古いデータに関するブラウザーアラートを表示します。

すべてを一緒に入れて

UpdateDialog React コンポーネントを定義し、最上位の onUpdate 関数にうまくリンクしたら、最後のステップはコンポーネントの既存のレイアウトに接続することです。

前のセクションで作成された CreateDialog は、インスタンスが 1 つしかないため、EmployeeList の上部に配置されます。ただし、UpdateDialog は特定の従業員に直接結び付けられています。そのため、Employee React コンポーネントで以下にプラグインされていることがわかります。

例 21: src/main/js/app.js - 更新された従業員ダイアログ
class Employee extends React.Component {

	constructor(props) {
		super(props);
		this.handleDelete = this.handleDelete.bind(this);
	}

	handleDelete() {
		this.props.onDelete(this.props.employee);
	}

	render() {
		return (
			<tr>
				<td>{this.props.employee.entity.firstName}</td>
				<td>{this.props.employee.entity.lastName}</td>
				<td>{this.props.employee.entity.description}</td>
				<td>
					<UpdateDialog employee={this.props.employee}
								  attributes={this.props.attributes}
								  onUpdate={this.props.onUpdate}/>
				</td>
				<td>
					<button onClick={this.handleDelete}>Delete</button>
				</td>
			</tr>
		)
	}
}

このセクションでは、コレクションリソースの使用から個々のリソースの使用に切り替えます。従業員レコードのフィールドは、this.props.employee.entity にあります。ETag を見つけることができる this.props.employee.headers にアクセスできます。

Spring Data REST でサポートされている他のヘッダー(Last-Modified など (英語) )は、このシリーズの一部ではありません。この方法でデータを構造化すると便利です。

.entity および .headers の構造は、rest.js (GitHub) を選択した REST ライブラリとして使用する場合にのみ適切です。別のライブラリを使用する場合は、必要に応じて適応する必要があります。

実際の動作を見る

変更されたアプリケーションの動作を確認するには:

  1. ./mvnw spring-boot:run を実行してアプリケーションを開始します。

  2. ブラウザのタブを開き、http://localhost:8080 に移動します。

    次のイメージのようなページが表示されるはずです。

    conditional 1
  3. Frodo の編集ダイアログを開きます。

  4. ブラウザで別のタブを開き、同じレコードを取得します。

  5. 最初のタブでレコードを変更します。

  6. 2 番目のタブで変更を試みてください。

    次のイメージに示すように、ブラウザのタブが変更されます。

    conditional 2
conditional 3

これらの変更により、衝突を回避することでデータの整合性が向上します。

レビュー

本セクション :

  • JPA ベースの楽観的ロック用に @Version フィールドを使用してドメインモデルを構成しました。

  • 個々のリソースを取得するようにフロントエンドを調整しました。

  • ETag ヘッダーを個々のリソースから If-Match リクエストヘッダーにプラグインして、PUT を条件付きにしました。

  • リストに表示されている従業員ごとに新しい UpdateDialog をコーディングしました。

これをプラグインすると、他のユーザーとの衝突や編集内容の上書きを簡単に回避できます。

課題 ?

悪いレコードを編集しているときを知ることは確かに素晴らしいことです。ただし、送信をクリックして確認するまで待つことをお勧めしますか?

リソースをフェッチするロジックは、loadFromServer と onNavigate の両方で非常に似ています。コードの重複を避ける方法はありますか?

JSON スキーマメタデータを使用して、CreateDialog および UpdateDialog 入力を構築します。メタデータを使用して物事をより一般的なものにする他の場所を見ますか? Employee.java にさらに 5 つのフィールドを追加したいと想像してください。UI を更新するには何が必要ですか?

パート 4 - イベント

前のセクションでは、同じデータを編集するときに他のユーザーとの衝突を避けるために条件付きリフレッシュを導入しました。また、楽観的ロックを使用してバックエンドでデータをバージョン管理する方法も学びました。誰かが同じレコードを編集した場合、ページをリフレッシュしてリフレッシュを取得できるようになったときに通知を受け取りました。

それはいいです。しかし、さらに良いものを知っていますか?他の人がリソースを更新したときに UI を動的に応答させる。

このセクションでは、Spring Data REST の組み込みイベントシステムを使用して、バックエンドの変更を検出し、Spring の WebSocket サポートを通じてすべてのユーザーに更新を公開する方法を学習します。その後、データが更新されると、クライアントを動的に調整できます。

このリポジトリからコード (GitHub) を自由に入手して、フォローしてください。このセクションは、前のセクションのアプリケーションに基づいており、追加のものが追加されています。

Spring WebSocket サポートをプロジェクトに追加する

開始する前に、プロジェクトの pom.xml ファイルに依存関係を追加する必要があります。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

この依存関係は、Spring Boot の WebSocket スターターをもたらします。

Spring を使用した WebSockets の構成

Spring には強力な WebSocket サポートが付属しています。認識すべきことの 1 つは、WebSocket が非常に低レベルのプロトコルであることです。クライアントとサーバー間でデータを送信する手段を提供する以上のものはありません。推奨事項は、サブプロトコル(このセクションでは STOMP)を使用して、実際にデータとルートをエンコードすることです。

次のコードは、サーバー側で WebSocket サポートを構成します。

@Component
@EnableWebSocketMessageBroker (1)
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { (2)

	static final String MESSAGE_PREFIX = "/topic"; (3)

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) { (4)
		registry.addEndpoint("/payroll").withSockJS();
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) { (5)
		registry.enableSimpleBroker(MESSAGE_PREFIX);
		registry.setApplicationDestinationPrefixes("/app");
	}
}
1@EnableWebSocketMessageBroker は WebSocket サポートをオンにします。
2WebSocketMessageBrokerConfigurer は、基本機能を構成するための便利な基本クラスを提供します。
3MESSAGE_PREFIX は、すべてのメッセージのルートに付加するプレフィックスです。
4registerStompEndpoints() は、クライアントとサーバーがリンクするためのバックエンドでエンドポイントを構成するために使用されます(/payroll)。
5configureMessageBroker() は、サーバーとクライアント間でメッセージを中継するために使用されるブローカーを構成するために使用されます。

この構成を使用すると、Spring Data REST イベントをタップして、WebSocket を介して公開できます。

Spring Data REST イベントのサブスクライブ

Spring Data REST は、リポジトリで発生するアクションに基づいて、いくつかのアプリケーションイベント (英語) を生成します。次のコードは、これらのイベントのいくつかをサブスクライブする方法を示しています。

@Component
@RepositoryEventHandler(Employee.class) (1)
public class EventHandler {

	private final SimpMessagingTemplate websocket; (2)

	private final EntityLinks entityLinks;

	@Autowired
	public EventHandler(SimpMessagingTemplate websocket, EntityLinks entityLinks) {
		this.websocket = websocket;
		this.entityLinks = entityLinks;
	}

	@HandleAfterCreate (3)
	public void newEmployee(Employee employee) {
		this.websocket.convertAndSend(
				MESSAGE_PREFIX + "/newEmployee", getPath(employee));
	}

	@HandleAfterDelete (3)
	public void deleteEmployee(Employee employee) {
		this.websocket.convertAndSend(
				MESSAGE_PREFIX + "/deleteEmployee", getPath(employee));
	}

	@HandleAfterSave (3)
	public void updateEmployee(Employee employee) {
		this.websocket.convertAndSend(
				MESSAGE_PREFIX + "/updateEmployee", getPath(employee));
	}

	/**
	 * Take an {@link Employee} and get the URI using Spring Data REST's {@link EntityLinks}.
	 *
	 * @param employee
	 */
	private String getPath(Employee employee) {
		return this.entityLinks.linkForItemResource(employee.getClass(),
				employee.getId()).toUri().getPath();
	}

}
1@RepositoryEventHandler(Employee.class) は、このクラスにフラグを立てて、employees に基づいてイベントをトラップします。
2SimpMessagingTemplate および EntityLinks は、アプリケーションコンテキストからオートワイヤーされます。
3@HandleXYZ アノテーションは、イベントをリッスンする必要があるメソッドにフラグを立てます。これらのメソッドはパブリックでなければなりません。

これらの各ハンドラーメソッドは、SimpMessagingTemplate.convertAndSend() を呼び出して、WebSocket を介してメッセージを送信します。これは pub-sub アプローチであるため、接続されているすべてのコンシューマーに 1 つのメッセージが中継されます。

各メッセージのルートは異なり、複数のメッセージをクライアント上の別個の受信者に送信できるようにし、必要なオープン WebSocket は 1 つだけです(リソース効率の高いアプローチ)。

getPath() は、Spring Data REST の EntityLinks を使用して、特定のクラスタイプと ID のパスを検索します。クライアントのニーズに応えるために、この Link オブジェクトはパスが抽出された Java URI に変換されます。

EntityLinks には、単一であれコレクションであれ、さまざまなリソースのパスをプログラムで見つけるためのユーティリティメソッドがいくつか付属しています。

基本的に、作成、更新、および削除イベントをリッスンし、イベントが完了した後、それらのイベントの通知をすべてのクライアントに送信します。また、そのような操作が発生する前にインターセプトし、おそらくログに記録したり、何らかの理由でブロックしたり、ドメインオブジェクトを追加情報で装飾したりできます。(次のセクションでは、これの便利な使用方法を説明します。)

JavaScript WebSocket の構成

次のステップは、WebSocket イベントを消費するためのクライアント側コードを書くことです。メインアプリケーションの次のチャンクはモジュールをプルします。

var stompClient = require('./websocket-listener')

そのモジュールを以下に示します。

'use strict';

const SockJS = require('sockjs-client'); (1)
require('stompjs'); (2)

function register(registrations) {
	const socket = SockJS('/payroll'); (3)
	const stompClient = Stomp.over(socket);
	stompClient.connect({}, function(frame) {
		registrations.forEach(function (registration) { (4)
			stompClient.subscribe(registration.route, registration.callback);
		});
	});
}

module.exports.register = register;
1WebSockets を介して話すために SockJS JavaScript ライブラリを取り込みます。
2stomp-websocket JavaScript ライブラリをプルして、STOMP サブプロトコルを使用します。
3WebSocket をアプリケーションの /payroll エンドポイントに向けます。
4 提供された registrations の配列を反復処理して、それぞれがメッセージの到着時にコールバックをサブスクライブできるようにします。

各登録エントリには route と callback があります。次のセクションでは、イベントハンドラーを登録する方法を確認できます。

WebSocket イベントの登録

React では、コンポーネントの componentDidMount() 関数は、DOM でレンダリングされた後に呼び出されます。また、コンポーネントがオンラインになり、ビジネスの準備が整うため、WebSocket イベントに登録するのに適切なタイミングです。次のコードはそうします。

componentDidMount() {
	this.loadFromServer(this.state.pageSize);
	stompClient.register([
		{route: '/topic/newEmployee', callback: this.refreshAndGoToLastPage},
		{route: '/topic/updateEmployee', callback: this.refreshCurrentPage},
		{route: '/topic/deleteEmployee', callback: this.refreshCurrentPage}
	]);
}

最初の行は以前と同じで、すべての従業員がページサイズを使用してサーバーから取得されます。2 行目は、WebSocket イベント用に登録されている JavaScript オブジェクトの配列を示しています。各オブジェクトには route と callback があります。

新しい従業員が作成されると、動作はデータセットをリフレッシュし、ページングリンクを使用して last ページに移動します。最後に移動する前にデータをリフレッシュするのはなぜですか?新しいレコードを追加すると、新しいページが作成される可能性があります。これが起こるかどうかを計算することは可能ですが、ハイパーメディアのポイントを覆します。カスタマイズされたページカウントを組み合わせるのではなく、既存のリンクを使用し、パフォーマンスを向上させる理由がある場合にのみその道をたどることをお勧めします。

従業員がリフレッシュまたは削除されると、現在のページがリフレッシュされます。レコードをリフレッシュすると、表示しているページに影響します。現在のページのレコードを削除すると、次のページのレコードが現在のページに取り込まれるため、現在のページもリフレッシュする必要があります。

これらの WebSocket メッセージが /topic で始まるという要件はありません。pub-sub セマンティクスを示す一般的な規則です。

次のセクションでは、これらの操作を実行する実際の操作を確認できます。

WebSocket イベントへの反応と UI 状態の更新

次のコードチャンクには、WebSocket イベントを受信したときに UI 状態を更新するために使用される 2 つのコールバックが含まれています。

refreshAndGoToLastPage(message) {
	follow(client, root, [{
		rel: 'employees',
		params: {size: this.state.pageSize}
	}]).done(response => {
		if (response.entity._links.last !== undefined) {
			this.onNavigate(response.entity._links.last.href);
		} else {
			this.onNavigate(response.entity._links.self.href);
		}
	})
}

refreshCurrentPage(message) {
	follow(client, root, [{
		rel: 'employees',
		params: {
			size: this.state.pageSize,
			page: this.state.page.number
		}
	}]).then(employeeCollection => {
		this.links = employeeCollection.entity._links;
		this.page = employeeCollection.entity.page;

		return employeeCollection.entity._embedded.employees.map(employee => {
			return client({
				method: 'GET',
				path: employee._links.self.href
			})
		});
	}).then(employeePromises => {
		return when.all(employeePromises);
	}).then(employees => {
		this.setState({
			page: this.page,
			employees: employees,
			attributes: Object.keys(this.schema.properties),
			pageSize: this.state.pageSize,
			links: this.links
		});
	});
}

refreshAndGoToLastPage() は、おなじみの follow() 機能を使用して、size パラメーターが適用された employees リンクにナビゲートし、this.state.pageSize をプラグインします。レスポンスを受信したら、最後のセクションから同じ onNavigate() 関数を呼び出して、新しいレコードが見つかる last ページにジャンプします。

refreshCurrentPage() も follow() 関数を使用しますが、this.state.pageSize を size に、this.state.page.number を page に適用します。これにより、現在表示している同じページが取得され、それに応じて状態が更新されます。

この動作は、リフレッシュまたは削除メッセージが送信されたときにすべてのクライアントに現在のページをリフレッシュするように指示します。現在のページが現在のイベントとは関係がない可能性があります。ただし、それを把握するのは難しい場合があります。削除されたレコードがページ 2 にあり、ページ 3 を見ている場合はどうなるでしょうか?すべてのエントリが変更されます。しかし、これは望ましい動作なのでしょうか?多分。そうでないかもしれない。

ローカル更新から状態管理を移動する

このセクションを完了する前に、認識すべきことがあります。UI の状態を更新する新しい方法、つまり WebSocket メッセージが到着したときを追加しました。しかし、状態を更新する古い方法はまだそこにあります。

コードの状態管理を簡素化するには、古い方法を削除します。つまり、POSTPUT および DELETE 呼び出しを送信しますが、それらの結果を使用して UI の状態を更新しないでください。代わりに、WebSocket イベントが循環するのを待ってから、更新を実行してください。

次のコードのチャンクは、前のセクションと同じ onCreate() 関数を示していますが、単純化されています:

onCreate(newEmployee) {
	follow(client, root, ['employees']).done(response => {
		client({
			method: 'POST',
			path: response.entity._links.self.href,
			entity: newEmployee,
			headers: {'Content-Type': 'application/json'}
		})
	})
}

ここでは、follow() 関数を使用して employees リンクに到達し、POST 操作が適用されます。以前のように、client({method: 'GET' …​ }) に then() または done() がないことに注意してください。更新をリッスンするイベントハンドラーは、今見た refreshAndGoToLastPage() にあります。

すべてを一緒に入れて

これらのすべての変更を適切に行ったら、アプリケーション(./mvnw spring-boot:run)を起動し、アプリケーションを調べます。2 つのブラウザタブを開き、サイズを変更して、両方を表示できるようにします。1 つで更新を開始し、他のタブがどのように即座に更新されるかを確認します。携帯電話を開き、同じページにアクセスします。友人を見つけて、その人に同じことをするように頼みます。このタイプの動的更新は、より鋭いものになるかもしれません。

挑戦したいですか? 2 つの異なるブラウザタブで同じレコードを開く前のセクションの演習を試してください。一方で更新してみて、もう一方で更新が表示されないようにしてください。可能であれば、条件付き PUT コードで保護する必要があります。しかし、それを停止のは難しいかもしれません!

レビュー

このセクションでは、次のことを行います。

  • SockJS フォールバックで Spring の WebSocket サポートを構成しました。

  • UI を動的に更新するために、Spring Data REST からイベントを作成、更新、削除するためにサブスクライブしました。

  • 影響を受ける REST リソースの URI をコンテキストメッセージ(「/topic/newEmployee」、「/topic/updateEmployee」など)とともに公開しました。

  • これらのイベントをリッスンするために WebSocket リスナーを UI に登録しました。

  • リスナーをハンドラーに接続して、UI の状態を更新しました。

これらのすべての機能を使用すると、2 つのブラウザーを並べて実行し、一方の更新が他方の更新にどのように反映されるかを簡単に確認できます。

課題 ?

複数のディスプレイはうまく更新されますが、正確な動作を磨くことが保証されます。例:新しいユーザーを作成すると、すべてのユーザーが最後までジャンプします。これをどのように扱うべきかについての考えはありますか?

ページングは便利ですが、管理が難しい状態です。このサンプルアプリケーションのコストは低く、React は UI で多くのちらつきを引き起こすことなく DOM を更新するのに非常に効率的です。しかし、より複雑なアプリケーションでは、これらのアプローチのすべてが適合するわけではありません。

ページングを念頭に置いて設計する場合、クライアント間で予想される動作と、更新が必要かどうかを決定する必要があります。システムの要件とパフォーマンスによっては、既存のナビゲーションハイパーメディアで十分な場合があります。

パート 5 - UI と API の保護

前のセクションでは、Spring Data REST の組み込みイベントハンドラーと Spring Framework の WebSocket サポートを使用して、他のユーザーからの更新にアプリを動的にレスポンスさせました。ただし、適切なユーザーのみが UI とその背後にあるリソースにアクセスできるように、すべてを保護しないと完全なアプリケーションはありません。

このリポジトリからコード (GitHub) を自由に入手して、フォローしてください。このセクションは、前のセクションのアプリに基づいて追加のものが追加されています。

Spring Security をプロジェクトに追加する

開始する前に、プロジェクトの pom.xml ファイルにいくつかの依存関係を追加する必要があります。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>org.thymeleaf.extras</groupId>
	<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

これにより、Spring Boot の Spring Security スターターに加えて、Web ページでセキュリティルックアップを行うための追加の Thymeleaf タグが追加されます。

セキュリティモデルの定義

過去のセクションでは、素晴らしい給与システムを使用してきました。バックエンドで物事を宣言し、Spring Data REST に面倒をさせるのは便利です。次のステップは、セキュリティ管理策を講じる必要があるシステムをモデル化することです。

これが給与システムの場合、管理者のみがアクセスします。Manager オブジェクトをモデリングして、物事を始めましょう。

@Entity
public class Manager {

	public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); (1)

	private @Id @GeneratedValue Long id; (2)

	private String name; (2)

	private @JsonIgnore String password; (2)

	private String[] roles; (2)

	public void setPassword(String password) { (3)
		this.password = PASSWORD_ENCODER.encode(password);
	}

	protected Manager() {}

	public Manager(String name, String password, String... roles) {

		this.name = name;
		this.setPassword(password);
		this.roles = roles;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;
		Manager manager = (Manager) o;
		return Objects.equals(id, manager.id) &&
			Objects.equals(name, manager.name) &&
			Objects.equals(password, manager.password) &&
			Arrays.equals(roles, manager.roles);
	}

	@Override
	public int hashCode() {

		int result = Objects.hash(id, name, password);
		result = 31 * result + Arrays.hashCode(roles);
		return result;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getPassword() {
		return password;
	}

	public String[] getRoles() {
		return roles;
	}

	public void setRoles(String[] roles) {
		this.roles = roles;
	}

	@Override
	public String toString() {
		return "Manager{" +
			"id=" + id +
			", name='" + name + '\'' +
			", roles=" + Arrays.toString(roles) +
			'}';
	}
}
1PASSWORD_ENCODER は、新しいパスワードを暗号化するか、パスワード入力を取得して比較前に暗号化する手段です。
2idnamepassword および roles は、アクセスを制限するために必要なパラメーターを定義します。
3 カスタマイズされた setPassword() メソッドにより、パスワードが平文で保存されることはありません。

セキュリティ層を設計する際に留意すべき重要な点があります。データ(パスワードなど)の適切な部分を保護し、コンソールに出力したり、ログに記録したり、JSON 直列化を通じてエクスポートしたりしないでください。

  • パスワードフィールドに適用される @JsonIgnore は、このフィールドを直列化する Jackson から保護します。

マネージャーのリポジトリを作成する

Spring Data は、エンティティの管理が非常に得意です。これらのマネージャーを処理するリポジトリを作成してみませんか?次のコードはそうします。

@RepositoryRestResource(exported = false)
public interface ManagerRepository extends Repository<Manager, Long> {

	Manager save(Manager manager);

	Manager findByName(String name);

}

通常の CrudRepository を継承する代わりに、それほど多くのメソッドは必要ありません。代わりに、データ(更新にも使用される)を保存し、既存のユーザーを検索する必要があります。Spring Data Common の最小限の Repository マーカーインターフェースを使用できます。定義済みの操作はありません。

Spring Data REST は、デフォルトで、見つかったリポジトリをエクスポートします。このリポジトリを REST 操作用に公開することは望ましくありません。@RepositoryRestResource(exported = false) アノテーションを適用して、エクスポートをブロックします。これにより、リポジトリとそのメタデータが提供されなくなります。

従業員とマネージャーとのリンク

セキュリティのモデリングの最後の部分は、従業員をマネージャーに関連付けることです。このドメインでは、従業員は 1 人のマネージャーを持つことができ、マネージャーは複数の従業員を持つことができます。次のコードはその関連を定義しています。

@Entity
public class Employee {

	private @Id @GeneratedValue Long id;
	private String firstName;
	private String lastName;
	private String description;

	private @Version @JsonIgnore Long version;

	private @ManyToOne Manager manager; (1)

	private Employee() {}

	public Employee(String firstName, String lastName, String description, Manager manager) { (2)
		this.firstName = firstName;
		this.lastName = lastName;
		this.description = description;
		this.manager = manager;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;
		Employee employee = (Employee) o;
		return Objects.equals(id, employee.id) &&
			Objects.equals(firstName, employee.firstName) &&
			Objects.equals(lastName, employee.lastName) &&
			Objects.equals(description, employee.description) &&
			Objects.equals(version, employee.version) &&
			Objects.equals(manager, employee.manager);
	}

	@Override
	public int hashCode() {

		return Objects.hash(id, firstName, lastName, description, version, manager);
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public Long getVersion() {
		return version;
	}

	public void setVersion(Long version) {
		this.version = version;
	}

	public Manager getManager() {
		return manager;
	}

	public void setManager(Manager manager) {
		this.manager = manager;
	}

	@Override
	public String toString() {
		return "Employee{" +
			"id=" + id +
			", firstName='" + firstName + '\'' +
			", lastName='" + lastName + '\'' +
			", description='" + description + '\'' +
			", version=" + version +
			", manager=" + manager +
			'}';
	}
}
1 マネージャー属性は、JPA の @ManyToOne 属性によってリンクされています。Manager は @OneToMany を必要としません。これは、ルックアップの必要性を定義していないためです。
2 ユーティリティコンストラクター呼び出しが更新され、初期化がサポートされます。

従業員をマネージャーに保護する

Spring Security は、セキュリティポリシーの定義に関する多数のオプションをサポートしています。このセクションでは、マネージャーのみが従業員の給与データを表示でき、保存、更新、削除の操作は従業員のマネージャーに限定されるように制限します。つまり、どのマネージャーもログインしてデータを表示できますが、変更を加えることができるのは特定の従業員のマネージャーだけです。次のコードはこれらのゴールを達成します。

@PreAuthorize("hasRole('ROLE_MANAGER')") (1)
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {

	@Override
	@PreAuthorize("#employee?.manager == null or #employee?.manager?.name == authentication?.name")
	Employee save(@Param("employee") Employee employee);

	@Override
	@PreAuthorize("@employeeRepository.findById(#id)?.manager?.name == authentication?.name")
	void deleteById(@Param("id") Long id);

	@Override
	@PreAuthorize("#employee?.manager?.name == authentication?.name")
	void delete(@Param("employee") Employee employee);

}
1 インターフェースの上部にある @PreAuthorize は、ROLE_MANAGER を持つ人々へのアクセスを制限します。

save() では、従業員のマネージャーが null(マネージャーが割り当てられていない場合の新しい従業員の最初の作成)であるか、従業員のマネージャーの名前が現在認証されているユーザーの名前と一致します。ここでは、Spring Security の SpEL 式を使用してアクセスを定義しています。null チェックを処理するための便利な ?. プロパティナビゲーターが付属しています。HTTP 操作とメソッドをリンクする引数で @Param(…​) を使用することに注意することも重要です。

delete() では、メソッドは従業員にアクセスするか、または id のみを持っている場合、アプリケーションコンテキストで employeeRepository を見つけ、findOne(id) を実行し、現在認証されているユーザーに対してマネージャーをチェックする必要があります。

UserDetails サービスの作成

セキュリティとの統合の一般的なポイントは、UserDetailsService を定義することです。これは、ユーザーのデータストアを Spring Security インターフェースに接続する方法です。Spring Security には、セキュリティチェックのためにユーザーを検索する方法が必要です。これがブリッジです。ありがたいことに、Spring Data を使用すると、その労力はごくわずかです。

@Component
public class SpringDataJpaUserDetailsService implements UserDetailsService {

	private final ManagerRepository repository;

	@Autowired
	public SpringDataJpaUserDetailsService(ManagerRepository repository) {
		this.repository = repository;
	}

	@Override
	public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
		Manager manager = this.repository.findByName(name);
		return new User(manager.getName(), manager.getPassword(),
				AuthorityUtils.createAuthorityList(manager.getRoles()));
	}

}

SpringDataJpaUserDetailsService は Spring Security の UserDetailsService を実装しています。インターフェースには 1 つのメソッド loadUserByUsername() があります。このメソッドは、Spring Security がユーザーの情報を調べることができるように、UserDetails オブジェクトを返すことを目的としています。

ManagerRepository があるため、この必要なデータをフェッチするために SQL または JPA 式を記述する必要はありません。このクラスでは、コンストラクター注入によってオートワイヤーされます。

loadUserByUsername() は、先ほど書いたカスタムファインダー findByName() を利用します。次に、UserDetails インターフェースを実装する Spring Security User インスタンスを取り込みます。また、Spring Securiy の AuthorityUtils を使用して、文字列ベースのロールの配列から GrantedAuthority タイプの Java List に移行しています。

セキュリティポリシーの作成

リポジトリに適用される @PreAuthorize 式は access rules です。これらのルールは、セキュリティポリシーなしの無用です。

@Configuration
@EnableWebSecurity (1)
@EnableGlobalMethodSecurity(prePostEnabled = true) (2)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter { (3)

	@Autowired
	private SpringDataJpaUserDetailsService userDetailsService; (4)

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth
			.userDetailsService(this.userDetailsService)
				.passwordEncoder(Manager.PASSWORD_ENCODER);
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception { (5)
		http
			.authorizeRequests()
				.antMatchers("/built/**", "/main.css").permitAll()
				.anyRequest().authenticated()
				.and()
			.formLogin()
				.defaultSuccessUrl("/", true)
				.permitAll()
				.and()
			.httpBasic()
				.and()
			.csrf().disable()
			.logout()
				.logoutSuccessUrl("/");
	}

}

このコードには非常に複雑な部分があるため、最初にアノテーションと API について説明します。次に、それが定義するセキュリティポリシーについて説明します。

1@EnableWebSecurity は、Spring Boot に自動構成されたセキュリティポリシーを削除し、代わりにこのポリシーを使用するように指示します。クイックデモでは、自動構成されたセキュリティで問題ありません。しかし、現実のものであれば、自分でポリシーを作成する必要があります。
2@EnableGlobalMethodSecurity は、Spring Security の洗練された @Pre および @Post アノテーションでメソッドレベルのセキュリティをオンにします。
3 ポリシーを記述するための便利な基本クラスである WebSecurityConfigurerAdapter を継承しています。
4 フィールドインジェクションによって SpringDataJpaUserDetailsService をオートワイヤーし、configure(AuthenticationManagerBuilder) メソッドを介してプラグインします。Manager からの PASSWORD_ENCODER もセットアップされます。
5 重要なセキュリティポリシーは、configure(HttpSecurity) メソッド呼び出しを使用して Pure Java で記述されています。

セキュリティポリシーでは、先に定義したアクセスルールを使用してすべてのリクエストを承認するように規定されています。

  • antMatchers() にリストされているパスには、静的 Web リソースをブロックする理由がないため、無条件のアクセスが許可されます。

  • そのポリシーに一致しないものはすべて anyRequest().authenticated() に分類されます。つまり、認証が必要です。

  • これらのアクセスルールが設定されると、Spring Security はフォームベース認証(成功時のデフォルトは /)を使用し、ログインページへのアクセスを許可するように指示されます。

  • BASIC ログインも CSRF を無効にして設定されます。これは主にデモンストレーション用であり、慎重な分析のない本番システムには推奨されません。

  • ログアウトは、ユーザーを / に導くように構成されています。

BZZ 認証は、curl を試すときに便利です。curl を使用してフォームベースのシステムにアクセスするのは困難です。HTTP(HTTPS ではない)を介した任意のメカニズムで認証すると、ネットワーク上で資格情報が盗聴される危険性があることを認識することが重要です。CSRF はそのまま残すのに適したプロトコルです。BASIC および curl との対話を容易にするために無効になっています。本番環境では、オンのままにしておくことをお勧めします。

セキュリティの詳細を自動的に追加する

優れたユーザーエクスペリエンスの一部は、アプリケーションがコンテキストを自動的に適用できる場合です。この例では、ログインしたマネージャーが新しい従業員レコードを作成する場合、そのマネージャーがそれを所有することは理にかなっています。Spring Data REST のイベントハンドラーでは、ユーザーが明示的にリンクする必要はありません。また、ユーザーが誤って誤ったマネージャーにレコードを割り当てないようにします。SpringDataRestEventHandler はそれを処理します:

@Component
@RepositoryEventHandler(Employee.class) (1)
public class SpringDataRestEventHandler {

	private final ManagerRepository managerRepository;

	@Autowired
	public SpringDataRestEventHandler(ManagerRepository managerRepository) {
		this.managerRepository = managerRepository;
	}

	@HandleBeforeCreate
	@HandleBeforeSave
	public void applyUserInformationUsingSecurityContext(Employee employee) {

		String name = SecurityContextHolder.getContext().getAuthentication().getName();
		Manager manager = this.managerRepository.findByName(name);
		if (manager == null) {
			Manager newManager = new Manager();
			newManager.setName(name);
			newManager.setRoles(new String[]{"ROLE_MANAGER"});
			manager = this.managerRepository.save(newManager);
		}
		employee.setManager(manager);
	}
}
1@RepositoryEventHandler(Employee.class) は、このイベントハンドラーに Employee オブジェクトのみに適用されるフラグを立てます。@HandleBeforeCreate アノテーションは、データベースに書き込まれる前に、受信 Employee レコードを変更する機会を提供します。

この状況では、現在のユーザーのセキュリティコンテキストを検索して、ユーザーの名前を取得できます。その後、findByName() を使用して関連するマネージャーを検索し、マネージャーに適用できます。その人がまだシステムに存在しない場合、新しいマネージャーを作成するための少し余分な接着剤コードがあります。ただし、主にデータベースの初期化をサポートすることです。実際の運用システムでは、そのコードを削除し、代わりに DBA またはセキュリティ運用チームに依存してユーザーデータストアを適切に維持する必要があります。

マネージャーデータのプリロード

マネージャーの読み込みとこれらのマネージャーへの従業員のリンクは簡単です。

@Component
public class DatabaseLoader implements CommandLineRunner {

	private final EmployeeRepository employees;
	private final ManagerRepository managers;

	@Autowired
	public DatabaseLoader(EmployeeRepository employeeRepository,
						  ManagerRepository managerRepository) {

		this.employees = employeeRepository;
		this.managers = managerRepository;
	}

	@Override
	public void run(String... strings) throws Exception {

		Manager greg = this.managers.save(new Manager("greg", "turnquist",
							"ROLE_MANAGER"));
		Manager oliver = this.managers.save(new Manager("oliver", "gierke",
							"ROLE_MANAGER"));

		SecurityContextHolder.getContext().setAuthentication(
			new UsernamePasswordAuthenticationToken("greg", "doesn't matter",
				AuthorityUtils.createAuthorityList("ROLE_MANAGER")));

		this.employees.save(new Employee("Frodo", "Baggins", "ring bearer", greg));
		this.employees.save(new Employee("Bilbo", "Baggins", "burglar", greg));
		this.employees.save(new Employee("Gandalf", "the Grey", "wizard", greg));

		SecurityContextHolder.getContext().setAuthentication(
			new UsernamePasswordAuthenticationToken("oliver", "doesn't matter",
				AuthorityUtils.createAuthorityList("ROLE_MANAGER")));

		this.employees.save(new Employee("Samwise", "Gamgee", "gardener", oliver));
		this.employees.save(new Employee("Merry", "Brandybuck", "pony rider", oliver));
		this.employees.save(new Employee("Peregrin", "Took", "pipe smoker", oliver));

		SecurityContextHolder.clearContext();
	}
}

1 つの欠点は、このローダーの実行時に Spring Security がアクティブになり、アクセスルールが完全に有効になることです。従業員データを保存するには、Spring Security の setAuthentication() API を使用して、適切な名前とロールでこのローダーを認証する必要があります。最後に、セキュリティコンテキストがクリアされます。

保護された REST サービスのツアー

これらのすべての修正が完了したら、アプリケーション(./mvnw spring-boot:run)を開始し、次の curl(出力とともに表示)を使用して修正をチェックアウトできます。

$ curl -v -u greg:turnquist localhost:8080/api/employees/1
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
* Server auth using Basic with user 'greg'
> GET /api/employees/1 HTTP/1.1
> Host: localhost:8080
> Authorization: Basic Z3JlZzp0dXJucXVpc3Q=
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Set-Cookie: JSESSIONID=E27F929C1836CC5BABBEAB78A548DF8C; Path=/; HttpOnly
< ETag: "0"
< Content-Type: application/hal+json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 25 Aug 2015 15:57:34 GMT
<
{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "description" : "ring bearer",
  "manager" : {
    "name" : "greg",
    "roles" : [ "ROLE_MANAGER" ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/api/employees/1"
    }
  }
}

これは、最初のセクションで見たよりもはるかに詳細を示しています。まず、Spring Security はいくつかの HTTP プロトコルをオンにして、さまざまな攻撃ベクトル(プラグマ、期限切れ、X-Frame-Options など)から保護します。Authorization ヘッダーをレンダリングする -u greg:turnquist で BASIC 資格情報も発行しています。

すべてのヘッダーの中で、バージョン管理されたリソースから ETag ヘッダーを確認できます。

最後に、データ自体の内部に、新しい属性 manager が表示されます。名前とロールは含まれていますが、パスワードは含まれていないことがわかります。これは、そのフィールドで @JsonIgnore を使用しているためです。Spring Data REST はそのリポジトリをエクスポートしなかったため、その値はこのリソースでインライン化されます。次のセクションで UI を更新するときに、これを有効に活用します。

UI でのマネージャー情報の表示

バックエンドでこれらのすべての変更を行うと、フロントエンドでの更新に移行できます。まず、<Employee /> React コンポーネント内に従業員のマネージャーを表示できます。

class Employee extends React.Component {

	constructor(props) {
		super(props);
		this.handleDelete = this.handleDelete.bind(this);
	}

	handleDelete() {
		this.props.onDelete(this.props.employee);
	}

	render() {
		return (
			<tr>
				<td>{this.props.employee.entity.firstName}</td>
				<td>{this.props.employee.entity.lastName}</td>
				<td>{this.props.employee.entity.description}</td>
				<td>{this.props.employee.entity.manager.name}</td>
				<td>
					<UpdateDialog employee={this.props.employee}
								  attributes={this.props.attributes}
								  onUpdate={this.props.onUpdate}
								  loggedInManager={this.props.loggedInManager}/>
				</td>
				<td>
					<button onClick={this.handleDelete}>Delete</button>
				</td>
			</tr>
		)
	}
}

これは、this.props.employee.entity.manager.name の列を追加するだけです。

JSON スキーマメタデータのフィルタリング

データ出力にフィールドが表示される場合、JSON スキーマメタデータにエントリがあると想定しても安全です。次の抜粋で確認できます。

{
	...
    "manager" : {
      "readOnly" : false,
      "$ref" : "#/descriptors/manager"
    },
    ...
  },
  ...
  "$schema" : "https://json-schema.org/draft-04/schema#"
}

manager フィールドは、人々に直接編集してほしいものではありません。インライン化されているため、読み取り専用の属性として表示する必要があります。CreateDialog および UpdateDialog からインラインエントリを除外するには、loadFromServer() で JSON スキーマメタデータを取得した後にそのようなエントリを削除できます。

/**
 * Filter unneeded JSON Schema properties, like uri references and
 * subtypes ($ref).
 */
Object.keys(schema.entity.properties).forEach(function (property) {
	if (schema.entity.properties[property].hasOwnProperty('format') &&
		schema.entity.properties[property].format === 'uri') {
		delete schema.entity.properties[property];
	}
	else if (schema.entity.properties[property].hasOwnProperty('$ref')) {
		delete schema.entity.properties[property];
	}
});

this.schema = schema.entity;
this.links = employeeCollection.entity._links;
return employeeCollection;

このコードは、URI 関係と $ref エントリの両方を削除します。

不正アクセスのトラップ

バックエンドでセキュリティチェックを設定すると、誰かが認可なしにレコードを更新しようとした場合に備えてハンドラーを追加できます。

onUpdate(employee, updatedEmployee) {
	if(employee.entity.manager.name === this.state.loggedInManager) {
		updatedEmployee["manager"] = employee.entity.manager;
		client({
			method: 'PUT',
			path: employee.entity._links.self.href,
			entity: updatedEmployee,
			headers: {
				'Content-Type': 'application/json',
				'If-Match': employee.headers.Etag
			}
		}).done(response => {
			/* Let the websocket handler update the state */
		}, response => {
			if (response.status.code === 403) {
				alert('ACCESS DENIED: You are not authorized to update ' +
					employee.entity._links.self.href);
			}
			if (response.status.code === 412) {
				alert('DENIED: Unable to update ' + employee.entity._links.self.href +
					'. Your copy is stale.');
			}
		});
	} else {
		alert("You are not authorized to update");
	}
}

HTTP 412 エラーをキャッチするコードがありました。これにより、HTTP 403 ステータスコードがトラップされ、適切なアラートが提供されます。

削除操作でも同じことができます。

onDelete(employee) {
	client({method: 'DELETE', path: employee.entity._links.self.href}
	).done(response => {/* let the websocket handle updating the UI */},
	response => {
		if (response.status.code === 403) {
			alert('ACCESS DENIED: You are not authorized to delete ' +
				employee.entity._links.self.href);
		}
	});
}

これは、調整されたエラーメッセージで同様にコーディングされます。

セキュリティの詳細を UI に追加する

このバージョンのアプリケーションを飾る最後のことは、react<div> の前にある index.html ファイルにこの新しい <div> を含めることにより、ログインしているユーザーを表示し、ログアウトボタンを提供することです。

<div>
    Hello, <span id="managername" th:text="${#authentication.name}">user</span>.
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Log Out"/>
    </form>
</div>

すべてを一緒に入れて

フロントエンドでこれらの変更を確認するには、アプリケーションを再起動して http://localhost:8080 に移動します。

すぐにログインフォームにリダイレクトされます。このフォームは Spring Security によって提供されますが、必要に応じて独自のフォームを作成できます。次の図に示すように、greg / turnquist としてログインします。

security 1

新しく追加されたマネージャー列が表示されます。次の図に示すように、oliver が所有する従業員が見つかるまで、数ページ進みます。

security 2

Update をクリックして、いくつかの変更を行い、もう一度 Update をクリックします。次のポップアップで失敗するはずです。

security 3

Delete を試みると、同様のメッセージで失敗するはずです。新しい従業員を作成する場合、あなたに割り当てられるべきです。

レビュー

このセクションでは、次のことを行います。

  • manager のモデルを定義し、1 対多の関連を介して従業員にリンクしました。

  • マネージャー用のリポジトリを作成し、Spring Data REST にエクスポートしないように指示しました。

  • 従業員リポジトリの一連のアクセスルールを作成し、セキュリティポリシーも作成します。

  • 別の Spring Data REST イベントハンドラーを作成して、発生する前に作成イベントをトラップし、現在のユーザーを従業員のマネージャーとして割り当てることができます。

  • UI を更新して、従業員の上司を表示し、不正な操作が行われたときにエラーポップアップを表示するようにしました。

課題 ?

Web ページは非常に洗練されています。しかし、関連とインラインデータの管理についてはどうでしょうか。作成および更新ダイアログは、実際には適していません。いくつかのカスタム記述フォームが必要になる場合があります。

管理者は従業員データにアクセスできます。従業員はアクセスできるべきですか?電話番号や住所などの詳細を追加する場合、どのようにモデル化しますか?特定のフィールドを更新できるように、従業員にシステムへのアクセスをどのように許可しますか?ページに配置するのに便利なハイパーメディアコントロールが他にありますか?

新しいガイドを作成したり、既存のガイドに貢献したいですか? 投稿ガイドラインを参照してください (GitHub)

すべてのガイドは、コード用の ASLv2 ライセンス、およびドキュメント用の Attribution、NoDerivatives クリエイティブコモンズライセンス (英語) でリリースされています。