angular-spring-data-restでSpring Data RESTにアクセス

# はじめに

Spring Data RESTを扱うのに便利な、angular-spring-data-restというAngularJSのmoduleが公開されています。
それを使ってSpring Data RESTが公開するAPIにアクセスする、最も簡単なWebアプリケーションを作成します。

# 参考

angular-spring-data-rest
https://github.com/guylabs/angular-spring-data-rest

# 前提条件

下の記事の後の状態を前提にしています。環境も同様です。
[Spring Data RESTでMySQLにアクセス](http://qiita.com/quwahara/items/4bfc3da62bd3a6c94e2a)

# 手順

## 依存するLibraryを追加する

angular-spring-data-restを使えるように、ビルドスクリプトの依存物として、// Addを追記します。

```groovy:build.gradle
dependencies {
	compile 'org.webjars:jquery:2.2.3'
	compile 'org.webjars:angularjs:1.5.3'
	compile 'org.webjars:angular-ui-router:0.2.18'
	compile 'org.webjars:bootstrap:3.3.6'
	// Add
	compile 'org.webjars.bower:angular-spring-data-rest:0.4.4'

	compile('org.springframework.boot:spring-boot-starter-data-jpa')
	compile('org.springframework.boot:spring-boot-starter-data-rest')
	compile('org.springframework.boot:spring-boot-starter-web')
	testCompile('org.springframework.boot:spring-boot-starter-test')

	compile('mysql:mysql-connector-java')
}
```

追記を反映するために、Package ExplorerでSSP37(プロジェクトルート)を選び右クリック、
Gradle -> Refresh Gradle Project
をクリックします。
Package ExplorerのProject and External Dependenciesに、追記したJarあるか確認します。

### 参考

WebJars http://www.webjars.org/

## Wordを一覧表示する

[前回で作ったWord Entity](http://qiita.com/quwahara/items/4bfc3da62bd3a6c94e2a#entity%E3%82%92%E4%BD%9C%E3%82%8B)を一覧表示してみます。

### index.htmlを書き換える

まずはメニュー項目とJavaScript Library参照の追加を行います。
APIを操作するページへのリンクとして、wordsというメニューを追加します。
angular-spring-data-restを使うために、angular-resource.min.jsとangular-spring-data-rest.min.jsへの参照を追加しています。

```html:src/main/resources/static/index.html
<html ng-app="ssp37App">
<head>
<meta charset="utf-8">
<title>SSP37</title>
<link rel="stylesheet" href="webjars/bootstrap/3.3.6/css/bootstrap.min.css">
</head>
<body>
	<div class="container">
		<ul class="nav nav-pills" role="tablist">
			<!-- Add -->
			<li><a ui-sref="words"	>words</a></li>
			<li><a ui-sref="hello"	>hello</a></li>
			<li><a ui-sref="blue"  	>blue</a></li>
			<li><a ui-sref="green"	>green</a></li>
		</ul>
	</div>
	<div ui-view class="container"></div>
	<script src="webjars/jquery/2.2.3/jquery.min.js"></script>
	<script src="webjars/bootstrap/3.3.6/js/bootstrap.min.js"></script>
	<script src="webjars/angularjs/1.5.3/angular.min.js"></script>
	<!-- Add -->
	<script src="webjars/angularjs/1.5.3/angular-resource.min.js"></script>
	<!-- Add -->
	<script src="webjars/angular-spring-data-rest/0.4.4/dist/angular-spring-data-rest.min.js"></script>
	<script src="webjars/angular-ui-router/0.2.18/angular-ui-router.min.js"></script>
	<script src="js/app.js"></script>
</body>
</html>
```

### moduleに依存物を追加する。

angular-spring-data-restを使えるように、自分のmoduleの依存物としてに、`ngResource`と`spring-data-rest`を追加します。

```js:src/main/resources/static/js/app.js
// Change
angular.module('ssp37App', ['ui.router' ,'ngResource' ,'spring-data-rest'])
    // Omit
    ;
```

### words stateを追加する。

追加したメニュー項目のwordsに対応するstateを導入します。
wordsはWord Entityへアクセスするページになります。
resolveでWordを一覧表示するためのデータ取得を実装します。
angular-spring-data-restが提供する`SpringDataRestAdapter`を使って実装しています。
この記述によって`/api/words`へGETした結果を、processedWordsという名前でWordsCtrl内で利用できるようになります。

```js:src/main/resources/static/js/app.js
angular.module('ssp37App', ['ui.router' ,'ngResource' ,'spring-data-rest'])
    .config(function($stateProvider, $urlRouterProvider) {
        // Omit
        $stateProvider
            // Add
            .state('words', {
                url: "/words",
                templateUrl: "templates/words.html",
                controller: "WordsCtrl",
                resolve: {
                    processedWords: function($http, SpringDataRestAdapter) {
                        return SpringDataRestAdapter.process($http.get("/api/words"));
                    },
                }
            })
            // Add
            ;
    })
    // Omit
    ;
```

### WordsCtrlを作る

words stateで使うControllerを実装します。
resolveで処理した`/api/words`へのGETの結果が、`processedWords`へ注入されてきます。
それを$scopeへ公開して、テンプレートで使えるようにします。

```js:src/main/resources/static/js/app.js
angular.module('ssp37App', ['ui.router' ,'ngResource' ,'spring-data-rest'])
    // Add
    .controller('WordsCtrl', function($scope, processedWords) {
        $scope.processedWords = processedWords
    })
    // Omit
    ;
```

### 一覧表示のテンプレートを作る

words stateで使うテンプレートを実装します。メニュー項目wordsのリンク先に当たるページです。
WordsCtrlで実装した`processedWords`の`_embeddedItems`プロパティにWord Entityの配列があります。それをリストで表示します。

```html:src/main/resources/static/templates/words.html
<h1>Word List</h1>
<div>
	<ul>
		<li ng-repeat="word in processedWords._embeddedItems">
			<a herf="">{{word.spelling}}</a>
			<span ng-show="word.favorite">(Favorite)</span>
		</li>
	</ul>
</div>
```

例えばGETの結果が下のようだった場合に、angular-spring-data-restは`_embeddedItems`プロパティへ自動で_embedded.wordsの配列を割り当ててくれます。

```json
{
  "_embedded" : {
    "words" : [ {
      "spelling" : "apple",
      "favorite" : true,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/words/14"
        },
        "word" : {
          "href" : "http://localhost:8080/api/words/14"
        }
      }
    }, {
      "spelling" : "pineapple",
      "favorite" : false,
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/api/words/16"
        },
        "word" : {
          "href" : "http://localhost:8080/api/words/16"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/api/words/search/findBySpellingLikeOrderBySpellingAsc?spelling=%25apple"
    }
  }
}
```

### 表示を確認する

一覧表示出来るところまで実装したので、確認してみましょう。

http://localhost:8080/index.html#/words を開いてみます。
下のような表示になったでしょうか。

![スクリーンショット 2016-04-23 12.31.31.png](https://qiita-image-store.s3.amazonaws.com/0/77421/efdbb4fe-c693-4983-04e7-997fe0cceb7e.png)

## Wordを更新する

次は一覧表示したWordを更新できるようにしてみます。
一覧から更新したいWordを選択し、その内容を編集して保存できるようにします。

### 一覧を選択できるようにする

ng-clickディレクティブを\<li\>タグに追記しました。
一覧項目がクリックされたら、selectItem関数がそれを捕捉するようにししています。

```html:src/main/resources/static/templates/words.html
<h1>Word List</h1>
<div>
	<ul>
		<li ng-repeat="word in processedWords._embeddedItems">
			<!-- Change  -->
			<a herf="" ng-click="selectItem(word)">{{word.spelling}}</a>
			<span ng-show="word.favorite">(Favorite)</span>
		</li>
	</ul>
</div>
```

### 選択項目を編集できるようにする

先ほど選択した項目を詳細表示するフォームを、一覧表示の下に追記しました。
Update Wordボタンを押すと、\<form\>タグのupdate(editItem)関数がそのイベントを捕捉するようにしています。

```html:src/main/resources/static/templates/words.html
<h1>Word List</h1>
<!-- Omit  -->

<!-- Add  -->
<h1>Word Detail</h1>
<form ng-submit="update(editItem)">
  <div class="form-group">
    <label for="spelling">Spelling</label>
    <input type="text" class="form-control" id="spelling" placeholder="Spelling" ng-model="editItem.spelling">
  </div>
  <div class="checkbox">
    <label>
      <input type="checkbox" ng-model="editItem.favorite"> Favorite
    </label>
  </div>
  <button type="submit" class="btn btn-default" ng-disabled="editItem._links == null">Update Word</button>
</form>
```

### 編集をControllerに実装する

編集を実装したWordsCtrlを見て行きましょう。
WordsCtrlにrefreshScope関数を新たに加えました。一覧表示のときは表示して終わりでしたが、Word更新の実装では、Word更新後に再び一覧表示の内容を更新したいので、その内容更新や初期化処理をrefreshScope関数に括りだしました。
\$scope.selectItem関数が一覧項目を選択したときに実行されます。
\$scope.editItemというモデルに、選択項目の内容を転写しています。
編集は\$scope.editItemへ行われます。
\$scope.update関数が、formのng-submitディレクティブでSubmitに関連付けられ、変更内容をREST APIへPOSTします。`_resources`はAngularJSの[$resource](https://docs.angularjs.org/api/ngResource/service/$resource)のように使えるようです。
ただ\$resourceが既定でHTTP PUTを用意しおらず、Spring Data Restでは更新がPUTになっているため、それに対応するように`'update'`アクションにPUTを割り当てています。直後でその`'update'`を呼び出しています。
その後、最初の一覧表示と同じように、`SpringDataRestAdapter.process`でWordの一覧を取得し、refreshScopeで更新結果を一覧に反映しています。

```js:src/main/resources/static/js/app.js
angular.module('ssp37App', ['ui.router' ,'ngResource' ,'spring-data-rest'])
    // Omit

    // Change
    .controller('WordsCtrl', function($scope, $http, SpringDataRestAdapter, processedWords) {
        var refreshScope = function(processedWords) {
            $scope.processedWords = processedWords
            $scope.editItem = { spelling: "", favorite: false }
        }
        refreshScope(processedWords)
        $scope.selectItem = function(word) {
            angular.extend($scope.editItem, word)
        }
        $scope.update = function(item) {
            item._resources("self", null, {'update': { method: 'PUT' }})
            .update(item)
            .$promise
            .then(function() {
                return SpringDataRestAdapter.process($http.get($scope.processedWords._links.self.href));
            })
            .then(function(processedWords) {
                refreshScope(processedWords)
            })
            ;
        }
    })
    // Omit
    ;
```

### 更新を確認する

更新が出来るところまで実装したので、確認してみましょう。

http://localhost:8080/index.html#/words を開いてみます。
下のような表示になったでしょうか。

![スクリーンショット 2016-04-23 13.11.50.png](https://qiita-image-store.s3.amazonaws.com/0/77421/e7d5a9de-577a-1ad1-1545-a602a512cafc.png)

Word Listの項目をクリックし、Word DetailでSpellingや、Favoriteを変更して、Update Wordボタンを押してみて下さい。
一覧の内容が変更され、Word Detailは初期状態に戻るでしょうか。


## Wordを削除する

更新と合わせて削除できるようにしてみます。
更新と同様に一覧から選択したWordを、削除出来るようにします。

### 選択項目を削除できるようにする

詳細表示フォームのUpdate Wordボタンの下に、Delete Wordボタンを追加しました。
Delete Wordボタンを押すと、`delete(editItem)`関数がそのイベントを捕捉するようにしています。

```html:src/main/resources/static/templates/words.html
<h1>Word List</h1>
<!-- Omit  -->

<h1>Word Detail</h1>
<form ng-submit="update(editItem)">
  <div class="form-group">
    <label for="spelling">Spelling</label>
    <input type="text" class="form-control" id="spelling" placeholder="Spelling" ng-model="editItem.spelling">
  </div>
  <div class="checkbox">
    <label>
      <input type="checkbox" ng-model="editItem.favorite"> Favorite
    </label>
  </div>
  <button type="submit" class="btn btn-default" ng-disabled="editItem._links == null">Update Word</button>
  <!-- Add  -->
  <button type="button" class="btn btn-default" ng-disabled="editItem._links == null" ng-click="delete(editItem)">Delete Word</button>
</form>
```

### 削除をControllerに実装する

削除を実装したWordsCtrlを見て行きましょう。
\$scope.update関数の後に、\$scope.delete関数を追加しました。
その関数でAPIへDELETEします。更新と同様に`_resources`の`'delete'`アクションを呼び出しています。HTTP DELETEはAngularJSの[$resource](https://docs.angularjs.org/api/ngResource/service/$resource)が既定で用意しているので、そのままそれを使います。
呼び出し後の削除結果の一覧表示への反映は、Word更新のときと同じです。

```js:src/main/resources/static/js/app.js
angular.module('ssp37App', ['ui.router' ,'ngResource' ,'spring-data-rest'])
    // Omit

    .controller('WordsCtrl', function($scope, $http, SpringDataRestAdapter, processedWords) {
        var refreshScope = function(processedWords) {
            $scope.processedWords = processedWords
            $scope.editItem = { spelling: "", favorite: false }
        }
        refreshScope(processedWords)
        $scope.selectItem = function(word) {
            angular.extend($scope.editItem, word)
        }
        $scope.update = function(item) {
            item._resources("self", null, {'update': { method: 'PUT' }})
            .update(item)
            .$promise
            .then(function() {
                return SpringDataRestAdapter.process($http.get($scope.processedWords._links.self.href));
            })
            .then(function(processedWords) {
                refreshScope(processedWords)
            })
            ;
        }
        // Add
        $scope.delete = function(item) {
            item._resources("self")
            .delete()
            .$promise
            .then(function() {
                return SpringDataRestAdapter.process($http.get($scope.processedWords._links.self.href));
            })
            .then(function(processedWords) {
                refreshScope(processedWords)
            })
            ;
        }
    })
    // Omit
    ;
```

### 削除を確認する

削除が出来るところまで実装したので、確認してみましょう。

http://localhost:8080/index.html#/words を開いてみます。
下のような表示になったでしょうか。

![スクリーンショット 2016-04-23 16.42.35.png](https://qiita-image-store.s3.amazonaws.com/0/77421/c0454fe6-91ca-763c-ce77-bed36c26cecd.png)

Word Listの項目をクリックし、Word DetailでDelete Wordボタンを押してみて下さい。
一覧から選択項目が削除されるでしょうか。

## Wordを追加する

新しいWordを追加できるようにします。

### 新しいWordを入力できるようにする

詳細表示フォームの下に、登録フォームを追加しました。
Register Wordボタンを押すと、\<form\>タグのsave(newItem)関数がそのイベントを捕捉するようにしています。

```html:src/main/resources/static/templates/words.html
<h1>Word List</h1>
<!-- Omit  -->

<h1>Word Detail</h1>
<!-- Omit  -->

<h1>Word Registry</h1>
<form ng-submit="save(newItem)">
  <div class="form-group">
    <label for="spelling">Spelling</label>
    <input type="text" class="form-control" id="spelling" placeholder="Spelling" ng-model="newItem.spelling">
  </div>
  <div class="checkbox">
    <label>
      <input type="checkbox" ng-model="newItem.favorite"> Favorite
    </label>
  </div>
  <button type="submit" class="btn btn-default" ng-disabled="newItem.spelling.length == 0">Register Word</button>
</form>
```

### 登録をControllerに実装する

登録を実装したWordsCtrlを見て行きましょう。
\$scope.delete関数の後に、\$scope.save関数を追加しました。
save関数も基本的にdelete関数と同じ様になっています。
その関数で[$resource](https://docs.angularjs.org/api/ngResource/service/$resource)が既定で用意してくれている`'save'`アクションを行います。それはAPIへPOSTになります。
さらに同様に登録結果の一覧表示への反映をしています。
後処理としてrefreshScope関数に、登録項目のmodelになる\$scope.newItemを、初期化する処理を加えました。

```js:src/main/resources/static/js/app.js
angular.module('ssp37App', ['ui.router' ,'ngResource' ,'spring-data-rest'])
    // Omit

    .controller('WordsCtrl', function($scope, $http, SpringDataRestAdapter, processedWords) {
        var refreshScope = function(processedWords) {
            $scope.processedWords = processedWords
            $scope.editItem = { spelling: "", favorite: false }
            // Add
            $scope.newItem = { spelling: "", favorite: false }
        }
        refreshScope(processedWords)
        // Omit
        $scope.delete = function(item) {
            item._resources("self")
            .delete()
            .$promise
            .then(function() {
                return SpringDataRestAdapter.process($http.get($scope.processedWords._links.self.href));
            })
            .then(function(processedWords) {
                refreshScope(processedWords)
            })
            ;
        }
        // Add
        $scope.save = function(item) {
            var wordClass = $scope.processedWords._resources("self")
            wordClass.save(new wordClass(item))
            .$promise
            .then(function() {
                return SpringDataRestAdapter.process($http.get($scope.processedWords._links.self.href));
            })
            .then(function(processedWords) {
                refreshScope(processedWords)
            })
            ;
        }
    })
    // Omit
    ;
```

### 登録を確認する

登録の確認をしてみましょう。

http://localhost:8080/index.html#/words を開いてみます。
下のような表示になったでしょうか。

![スクリーンショット 2016-04-23 16.55.41.png](https://qiita-image-store.s3.amazonaws.com/0/77421/eb16a9a8-51eb-9762-b8d8-a5e1af19873c.png)

Word Registryの下のSpellingに適当に単語を入力し、Favoriteも好みで指定して、Register Wordボタンを押します。
一覧に登録項目が表示されるでしょうか。

# 参考

## Sample Repository
https://github.com/quwahara/SSP37/tree/700-rest-angular


## JSON Schema

ちなみに下のようにアクセスすると、EntityのSchema情報が取得できます。
[13.2. JSON Schema](http://docs.spring.io/spring-data/rest/docs/2.5.1.RELEASE/reference/html/#metadata.json-schema)

```bash
$ curl -H 'Accept:application/schema+json' http://localhost:8080/api/profile/words
{
  "title" : "Word",
  "properties" : {
    "spelling" : {
      "title" : "Spelling",
      "readOnly" : false,
      "type" : "string"
    },
    "favorite" : {
      "title" : "Favorite",
      "readOnly" : false,
      "type" : "boolean"
    }
  },
  "definitions" : { },
  "type" : "object",
  "$schema" : "http://json-schema.org/draft-04/schema#"
}
```

もしかして、この記事で実装したようなBoilerplateなコードを自動生成してくれるようなものが、何処かにあるのでしょうか。