このチュートリアルでは、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-approved link names(英語) など、広く受け入れられている業界標準プロトコルを使用します。

ドメインを宣言する

ドメインオブジェクトは、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を取得します。
4 run() メソッドはコマンドライン引数で呼び出され、データをロードします。

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 mwethodを作成することです。

例 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"
    }
  }
}

この関連ガイドに示すように、PUT, PATCHおよび 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>
	<version>1.6</version>
	<configuration>
		<installDirectory>target</installDirectory>
	</configuration>
	<executions>
		<execution>
			<id>install node and npm</id>
			<goals>
				<goal>install-node-and-npm</goal>
			</goals>
			<configuration>
				<nodeVersion>v10.11.0</nodeVersion>
				<npmVersion>6.4.1</npmVersion>
			</configuration>
		</execution>
		<execution>
			<id>npm install</id>
			<goals>
				<goal>npm</goal>
			</goals>
			<configuration>
				<arguments>install</arguments>
			</configuration>
		</execution>
		<execution>
			<id>webpack build</id>
			<goals>
				<goal>webpack</goal>
			</goals>
		</execution>
	</executions>
</plugin>

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

  • install-node-and-npm コマンドは、node.jsとそのパッケージ管理ツール npmtarget フォルダーにインストールします。(これにより、バイナリがソース管理下に置かれなくなり、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"
  },
  "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構成ファイル:

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

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

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

  • ES6 Reactコードを標準ブラウザで実行できる形式にコンパイルするために、es2015react の両方のプリセットを使用して、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)
1 React は、このアプリのビルドに使用されるFacebookのメインライブラリの1つです。
2 React は、このアプリのビルドに使用されるFacebookのメインライブラリの1つです。
3 client は、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}/>
		)
	}
}
1 class App extends React.Component{…​} は、Reactコンポーネントを作成するメソッドです。
2 componentDidMount は、ReactがDOMのコンポーネントをレンダリングした後に呼び出されるAPIです。
3 render は、画面上にコンポーネントを「描画」するAPIです。
Reactでは、大文字はコンポーネントの命名規則です。

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

React components have two types of data: state and properties.

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

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

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

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

このコードでは、関数はrest.jsのPromise-compliant(英語) インスタンスである 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} />

The preceding listing creates a new React component (note the uppercase format) with two properties: key and data. These are supplied with values from employee._links.self.href and 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 variable(英語) を必要としないようにします。

ロジックを構造に混ぜることが心配ですか? 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() accepts two arguments: a React component you defined as well as a DOM node to inject it into. Remember how you saw the <div id="react"></div> item earlier from the HTML page? This is where it gets picked up and plugged in.

これらすべてを準備したら、アプリケーション(./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. Fielding
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: 既存のリソースを変更する (partially rather than creating a new resource)

これらは、よく知られた仕様を持つ標準化された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人の従業員のみがリストされています。さらに、first, nextおよび 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がすぐに提供するハイパーメディアコントロールの使用を開始するために、バックエンドでこれ以上の変更は必要ありません。フロントエンドでの作業に切り替えることができます。(That is part of the beauty of Spring Data REST: No messy controller updates!)

このアプリケーションはnpt「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"> の内部には、入力フィールドの動的リストが挿入され、その後に作成ボタンが続くフォームがあります。そのボタンには 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ベースのレコードを取得し、DELETEself リンクに適用することだけです。

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は、ページサイズに基づいてナビゲーションリンクを流動的に更新します。

There is an HTML element at the top of ElementList.render : <input ref="pageSize" defaultValue={this.props.pageSize} onInput={this.handleInput}/> .

  • 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コンポーネントに送信します。そうでない場合は、入力した文字が入力から取り除かれます。

AppupdatePageSize()を取得するとどうなりますか?見てみな:

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
		});
	});
}
1 follow() 関数は、employees コレクションリソースに移動します。
2最初の then(employeeCollection ⇒ …​) 句は、JSONスキーマデータをフェッチする呼び出しを作成します。これには、メタデータとナビゲーションリンクを <App/> コンポーネントに保存するための内部then句があります。

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

32番目の then(employeeCollection ⇒ …​) 句は、従業員のコレクションを GET の配列に変換し、個々のリソースをフェッチすることを約束します。これは、各従業員のETagヘッダーを取得するために必要なものです
4 then(employeePromises ⇒ …​) 句は GET 約束の配列を取り、when.all()で単一の約束にマージします。これは、すべてのGET約束が解決されると解決されます。
5 loadFromServerdone(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() 機能にリンクされています。これは、handleSubmit() を使用してポップアップの詳細を抽出するために、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 request header(英語) を持つ PUT により、Spring Data RESTは現在のバージョンに対して値をチェックします。受信 If-Match 値がデータストアのバージョン値と一致しない場合、Spring Data RESTは HTTP 412 Precondition Failedで失敗します。

約束/ 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 をコーディングしました。

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

課題?

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

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

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サポートをオンにします。
2 WebSocketMessageBrokerConfigurer は、基本機能を構成するための便利な基本クラスを提供します。
3MESSAGE_PREFIXは、すべてのメッセージのルートに付加するプレフィックスです。
4 registerStompEndpoints() は、クライアントとサーバーがリンクするためのバックエンドでエンドポイントを構成するために使用されます(/payroll)。
5 configureMessageBroker() は、サーバーとクライアント間でメッセージを中継するために使用されるブローカーを構成するために使用されます。

この構成を使用すると、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) は、このクラスにフラグを立てて、従業員に基づいてイベントをトラップします。
2 SimpMessagingTemplate および EntityLinks は、アプリケーションコンテキストからオートワイヤーされます。
3 @HandleXYZ アノテーションは、イベントをリッスンする必要があるメソッドにフラグを立てます。これらのメソッドはパブリックでなければなりません。

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

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

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

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

基本的に、作成、更新、および削除イベントをリッスンし、イベントが完了した後、それらのイベントの通知をすべてのクライアントに送信します。また、そのような操作が発生する前にインターセプトし、おそらくログに記録したり、何らかの理由でブロックしたり、ドメインオブジェクトを追加情報で装飾したりできます。(In the next section, we will see a handy use for this.)

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 の配列を反復処理して、それぞれがメッセージの到着時にコールバックをサブスクライブできるようにします。

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

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オブジェクトの配列を示しています。各オブジェクトには routecallbackがあります。

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

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

これらの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() 関数を呼び出して、新しいレコードが見つかる最後のページにジャンプします。

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

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

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

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

コードの状態管理を簡素化するには、古い方法を削除します。つまり、POST, PUTおよび 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) +
			'}';
	}
}
1 PASSWORD_ENCODER は、新しいパスワードを暗号化するか、パスワード入力を取得して比較前に暗号化する手段です。
2 id, name, passwordおよび 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)を実行し、現在認証されているユーザーに対してマネージャーをチェックする必要があります。

Writing a UserDetails Service

A common point of integration with security is to define a UserDetailsService . This is the way to connect your user’s data store into a Spring Security interface. Spring Security needs a way to look up users for security checks, and this is the bridge. Thankfully, with Spring Data, the effort is quite minimal:

@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 implements Spring Security’s UserDetailsService . The interface has one method: loadUserByUsername() . This method is meant to return a UserDetails object so that Spring Security can interrogate the user’s information.

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

loadUserByUsername() taps into the custom finder you wrote a moment ago, findByName() . It then populates a Spring Security User instance, which implements the UserDetails interface. You are also using Spring Securiy’s AuthorityUtils to transition from an array of string-based roles into a Java List of type GrantedAuthority .

Wiring up Your Security Policy

The @PreAuthorize expressions applied to your repository are access rules. These rules are for nought without a security policy:

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

}

This code has a lot of complexity in it, so we will walk through it, first talking about the annotations and APIs. Then we will discuss the security policy it defines.

1 @EnableWebSecurity は、Spring Bootに自動構成されたセキュリティポリシーを削除し、代わりにこのポリシーを使用するように指示します。クイックデモでは、自動構成されたセキュリティで問題ありません。しかし、現実のものであれば、自分でポリシーを作成する必要があります。
2 @EnableGlobalMethodSecurity は、Spring Securityの洗練された @Pre および @Post アノテーションでメソッドレベルのセキュリティをオンにします。
3It extends WebSecurityConfigurerAdapter , a handy base class for writing policy.
4It autowires the SpringDataJpaUserDetailsService by field injection and then plugs it in through the configure(AuthenticationManagerBuilder) method. The PASSWORD_ENCODER from Manager is also set up.
5The pivotal security policy is written in pure Java with the configure(HttpSecurity) method call.

The security policy says to authorize all requests by using the access rules defined earlier:

  • The paths listed in antMatchers() are granted unconditional access, since there is no reason to block static web resources.

  • Anything that does not match that policy falls into anyRequest().authenticated() , meaning it requires authentication.

  • With those access rules set up, Spring Security is told to use form-based authentication (defaulting to / upon success) and to grant access to the login page.

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

  • Logout is configured to take the user to / .

BASIC authentication is handy when you are experimenting with curl. Using curl to access a form-based system is daunting. It is important to recognize that authenticating with any mechanism over HTTP (not HTTPS) puts you at risk of credentials being sniffed over the wire. CSRF is a good protocol to leave intact. It is disabled to make interaction with BASIC and curl easier. In production, it is best to leave it on.

Adding Security Details Automatically

One part of a good user experience is when the application can automatically apply context. In this example, if a logged-in manager creates a new employee record, it makes sense for that manager to own it. With Spring Data REST’s event handlers, there is no need for the user to explicitly link it. It also ensures the user does not accidentally assign records to the wrong manager. The SpringDataRestEventHandler handles that for us:

@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) flags this event handler as applying only to Employee objects. The @HandleBeforeCreate annotation gives you a chance to alter the incoming Employee record before it gets written to the database.

In this situation, you can look up the current user’s security context to get the user’s name. Then you can look up the associated manager by using findByName() and apply it to the manager. There is a little extra glue code to create a new manager if that person does not exist in the system yet. However, that is mostly to support initialization of the database. In a real production system, that code should be removed and instead depend on the DBAs or Security Ops team to properly maintain the user data store.

Pre-loading Manager Data

Loading managers and linking employees to these managers is straightforward:

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

The one wrinkle is that Spring Security is active with access rules in full force when this loader runs. Thus, to save employee data, you must use Spring Security’s setAuthentication() API to authenticate this loader with the proper name and role. At the end, the security context is cleared out.

Touring Your Secured REST Service

With all these modifications in place, you can start the application ( ./mvnw spring-boot:run ) and check out the modifications by using the following curl (shown with its output):

$ 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"
    }
  }
}

This shows a lot more detail than you saw in the first section. First of all, Spring Security turns on several HTTP protocols to protect against various attack vectors (Pragma, Expires, X-Frame-Options, and others). You are also issuing BASIC credentials with -u greg:turnquist which renders the Authorization header.

Amidst all the headers, you can see the ETag header from your versioned resource.

Finally, inside the data itself, you can see a new attribute: manager。You can see that it includes the name and roles but NOT the password. That is due to using @JsonIgnore on that field. Because Spring Data REST did not export that repository, its values are inlined in this resource. You will put that to good use as you update the UI in the next section.

Displaying Manager Information in the UI

With all these modifications in the backend, you can now shift to updating things in the frontend. First of all, you can show an employee’s manager inside the <Employee /> React component:

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の列を追加するだけです。

Filtering out JSON Schema Metadata

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

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

The manager field is not something you want people to edit directly. Since it is inlined, it should be viewed as a read-only attribute. To filter out inlined entries from the CreateDialog and UpdateDialog , you can delete such entries after fetching the JSON Schema metadata in loadFromServer() :

/**
 * 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エントリの両方を削除します。

Trapping for Unauthorized Access

With security checks configured on the backend, you can add a handler in case someone tries to update a record without authorization:

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ステータスコードがトラップされ、適切なアラートが提供されます。

You can do the same for delete operations:

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

This is coded similarly with a tailored error message.

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

The last thing to crown this version of the application is to display who is logged in as well provide a logout button by including this new <div> in the index.html file ahead of the react <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>

すべてを一緒に入れて

To see these changes in the frontend, restart the application and navigate to http://localhost:8080

You are immediately redirected to a login form. This form is supplied by Spring Security, though you can create your own if you wish. Log in as greg / turnquist , as the following image shows:

security 1

You can see the newly added manager column. Go through a couple pages until you find employees owned by oliver, as the following image shows:

security 2

Click on 更新 , make some changes, and then click 更新 again. It should fail with the following pop-up:

security 3

If you try 削除 , it should fail with a similar message. If you create a new employee, it should be assigned to you.

レビュー

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

  • Defined the model of manager and linked it to an employee through a 1-to-many relationship.

  • Created a repository for managers and told Spring Data REST to not export.

  • Wrote a set of access rules for the employee repository and also write a security policy.

  • Wrote another Spring Data REST event handler to trap creation events before they happen so that the current user could be assigned as the employee’s manager.

  • Updated the UI to show an employee’s manager and also display error pop-ups when unauthorized actions are taken.

課題?

The webpage has become quite sophisticated. But what about managing relationships and inlined data? The create and update dialogs are not really suited for that. It might require some custom written forms.

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

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

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