作って学ぶ Spring Boot アプリケーション(2) -開発編-

#はじめに

この記事は[作って学ぶ Spring Boot アプリケーション (1) -準備編-](https://qiita.com/sgr0416st/items/94a612a27c2f90260f17)の続きです。
こちらを確認していない方はまずご確認ください。
前回はSpring Bootアプリケーションの開発準備を整えてきました。
今回から実際にアプリケーションを開発していきます。

#要件
前回も書きましたが、改めてここに示しておきます。
###### データベース
学習教材と学習記録をデータとして保持して、それぞれ下記の要素を持っているものとします。

- 教材テーブル(Subject)
    - 項目ID(Long)
    - 学習項目(String)
    - タイプ(教科等)(String)
- 学習記録テーブル(Record)
    - 記録ID(Long)
    - 日付(Date)
    - 項目(Subject)
    - 時間(Double)
    - 備考(String)

######要件
以下の操作ができるようにします。

- 項目一覧の取得 () -> (List<Subject>)
- 項目の追加 (SubjectName, Type) -> (Subject)
- 項目の削除 (SubjectID) -> ()
- 記録一覧の取得 () -> (List<Record>)
- 記録の追加 (Date, SubjectID, Hours) -> (RecordID)
- 記録の削除 (RecordID) -> ()

#開発
それでは開発を初めていきます。
まず、controller、model、repository、serviceパッケージをそれぞれ並列に作成してください。

![スクリーンショット 2020-05-18 20.54.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271486/ece82001-7260-0d70-5180-1e53e607519f.png)

このリポジトリの意味について詳しく知りたい方は、ドメイン駆動設計等のキーワードで調べて見てください。

###モデルの作成
モデルは、データベースのエンティティ(1つのデータ)を表現します。
今回は、教材(Contents)と記録(Records)の２つのデータベースからなるので、それぞれに対応したモデルを作成していきます。
modelパッケージの下に、Contentクラス、Recordクラスをそれぞれ作成してください。

![スクリーンショット 2020-05-18 22.04.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271486/043293c9-887c-2f49-c066-745519d4d52c.png)


その後それぞれのファイルを, ${パッケージ}を適切な名前に書き換えて以下のように記述してください。

```Content.java
package ${パッケージ}.model;

import lombok.*;

import javax.persistence.*;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Data
@Entity
public class Content {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "content_id")
    private Long content_id;

    private String name;
    private String type;
}
```

```Record.java
package ${パッケージ}.model;

import lombok.*;

import javax.persistence.*;
import java.util.Date;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Data
@Entity
public class Record {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "record_id")
    private Long record_id;

    @ManyToOne
    @JoinColumn(name = "content")
    private Content content;

    private Date record_date;
    private Double study_hours;
    private String record_description;
}

```
コードについて簡単に説明します。

- ```@Builder, @AllArgsConstructor, @NoArgsConstructor, @ToString, @Data```
    - よく必要になるメソッド(ゲッター・セッター等)を自動で生成してくれるLombokのアノテーション
    - ビルダー、コンストラクタ等の関数が作成されている。気になる人は開発環境のアウトライン機能を用いて確かめられる。
    - Intellijを使用している人は事前にLombokプラグインをインストールしておく
- ```@Id, @GeneratedValue(strategy = GenerationType.AUTO), @Column(name = "content_id")```
    - JPAのアノテーション。GeneratedValueは主キーを自動生成してくれる
- ```@ManyToOne, @JoinColumn(name = "content")``` 
    - エンティティ間の関係性を表すアノテーション。教材：記録=１:多という関係に留意する

これでエンティティが定義出来ました。

###リポジトリの作成
次に、データベースを操作するリポジトリを作成します。
これにより、サービスはデータベースの実装に依らない設計にすることが出来ます。
repositoryパッケージの下に、ContentRepositoryインターフェース、RecordRepositoryインターフェースをそれぞれ作成してください。

![スクリーンショット 2020-05-18 22.04.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271486/4070ff0b-931b-c7d8-62aa-f2e75119d7ec.png)

その後それぞれのファイルを, ${パッケージ}を適切な名前に書き換えて以下のように記述してください。

```ContentRepository.java
package ${パッケージ}.repository;

import ${パッケージ}.model.Content;
import org.springframework.data.repository.CrudRepository;

public interface ContentRepository extends CrudRepository<Content, Long> {
}
```

```RecordRepository.java
package ${パッケージ}.repository;

import import ${パッケージ}.model.Record;
import org.springframework.data.repository.CrudRepository;

public interface RecordRepository extends CrudRepository<Record, Long> {
}
```

今回インターフェースを継承しただけですが、これで実はいい感じに色々なデータベースとやりとりしてくれるメソッドが実行時に作成されます。完成です。Springすごい...

###サービスの作成
次はデータをどのように取り扱うかを決定するサービスを作成します。
ここでは主にビジネスロジックを記述していきます。つまり、最初の要件で定義した機能をここで実装していきます。
serviceパッケージの下に、StudyRecordServiceインターフェース、StudyRecordServiceImplクラスをそれぞれ作成してください。

![スクリーンショット 2020-05-18 22.14.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271486/deb4b8fe-382b-7e1f-2806-b5663a4f19f9.png)

まずは要件定義をプログラムに落とし込むために、インターフェースを作りましょう。
ここでは、実際にどのような機能が欲しいかを記述していきます。
${パッケージ}を適切な名前に書き換えて以下のように記述してください。

```StudyRecordService.java
package ${パッケージ}.service;

import ${パッケージ}.model.Record;
import ${パッケージ}.model.Content;

import java.util.List;

public interface StudyRecordService {
    //項目一覧の取得
    public List<Content> getAllContents();
    //項目の追加
    public Content addContent(String name, String type);
    //項目の削除
    public void deleteContent(Long content_id);
    //記録一覧の取得
    public List<Record> getAllRecords();
    //記録の追加
    public Record addRecord(Content content, Double study_hours);
    //記録の削除
    public void deleteRecord(Long record_id);
}
```

次に、実際にこれらの機能をStudyRecordServiceImpl.java実装していきます。

```StudyRecordServiceImpl.java
package ${パッケージ}.service;

import ${パッケージ}.model.Record;
import ${パッケージ}.model.Content;
import ${パッケージ}.repository.RecordRepository;
import ${パッケージ}.repository.ContentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Optional;

@Service
@Transactional
public class StudyRecordServiceImpl implements StudyRecordService{
    @Autowired
    private ContentRepository contentRepository;
    @Autowired
    private RecordRepository recordRepository;

    @Override
    public List<Content> getAllContents() {
        return (List<Content>) contentRepository.findAll();
    }

    @Override
    public Content addContent(String name, String type) {
        return contentRepository.save(
                Content.builder().name(name).type(type).build()
        );
    }

    @Override
    public void deleteContent(Long content_id) {
        Optional<Content> content =  contentRepository.findById(content_id);
        if (!content.isPresent()){
            throw new IllegalArgumentException();
        }else{
            contentRepository.delete(content.get());
        }
    }

    @Override
    public List<Record> getAllRecords() {
        return (List<Record>) recordRepository.findAll();
    }

    @Override
    public Record addRecord(Content content, Double study_hours) {
        Date date = Calendar.getInstance().getTime();
        return recordRepository.save(
                Record.builder().record_date(date).content(content).study_hours(study_hours).build()
        );
    }

    @Override
    public void deleteRecord(Long record_id) {
        Optional<Record> record =  recordRepository.findById(record_id);
        if (!record.isPresent()){
            throw new IllegalArgumentException();
        }else{
            recordRepository.delete(record.get());
        }
    }
}

```

これで、ビジネスロジックの部分が完成しました。
少しだけ補足をします。
- ```@Service```
    - サービスであることをSpringに伝えるアノテーション。おまじない。
- ```@Autowired```
    - Springにインターフェースを適切な実装とマッチングしてもらうアノテーション。おまじない２。
- ```contentRepository, recordRepository```
    - それぞれ基本的なデータベース処理機能を持っている。具体的には以下参照
    - https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/CrudRepository.html

###コントローラの作成
最後に、リクエストとサービスのマッチング処理を担当するコントローラを実装していきます。
contorollerパッケージの下に、StudyRecordControllerクラスをそれぞれ作成してください。

![スクリーンショット 2020-05-18 22.22.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271486/15a81201-2609-87c3-5ebc-e6502510df68.png)

${パッケージ}を適切な名前に書き換えて以下のように記述してください。

```StudyRecordController.java
package ${パッケージ}.controller;

import ${パッケージ}.model.Record;
import ${パッケージ}.model.Content;
import ${パッケージ}.service.StudyRecordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("api/v1/")
public class StudyRecordController {

    @Autowired
    private StudyRecordService studyRecordService;
    
    @GetMapping
    @RequestMapping("contents")
    public List<Content> getAllContents(){
        return studyRecordService.getAllContents();
    }

    @PostMapping(path = "contents")
    public Content postSubject(@RequestBody Content content){
        return studyRecordService.addContent(content.getName(), content.getType());
    }
    @RequestMapping(value = "contents/{id}", method = RequestMethod.DELETE)
    public void deleteContent(@PathVariable Long id){
        System.out.println(id);
        studyRecordService.deleteContent(id);
    }

    @GetMapping
    @RequestMapping("records")
    public List<Record> getAllRecords(){
        return studyRecordService.getAllRecords();
    }
    @PostMapping(path = "records")
    public Record postRecord(@RequestBody Record record){
        return studyRecordService.addRecord(record.getContent(), record.getStudy_hours());
    }
    @RequestMapping(value = "records/{id}", method = RequestMethod.DELETE)
    public void deleteRecord(@PathVariable Long id){
        studyRecordService.deleteRecord(id);
    }

}
```

こちらにも簡単に補足していきます。

- ```@RestController, @RequestMapping("api/v1/")```
    - REST APIに準拠していることを表している。
    - 今回はルートをapi/v1/にしている。ここは自由。
- ```@RequestMapping, @GetMapping, @PostMapping```
    - それぞれの処理がどのURLの、どのタイプのリクエストが届いた時に実行されるかを記述しているアノテーション。例えば、api/v1/subjects　にGETリクエストを送ると、getAllSubjects()が実行され、その戻り値がレスポンスとして返される。
- ```@RequestBody, @PathVariable```
    - それぞれリクエストボディ、URLパラメータのデータを指定した型にマッピングするアノテーション。
    - リクエストボディに対応する値が格納されていない場合、マッピングするエンティティの該当フィールドはnullになる

これで、一通りの機能が完成しました。うまく動作するか確かめて見ましょう。

# 使ってみる
それでは、実際にシステムを使ってみましょう。
今回はPostmanを使って調べてみます。Postmmanはグラフィカルにリクエストを扱うことができる
curlとか使い慣れてるひとはそちらでも全然OK。

まずシステムを実行しましょう。
うまく実行できれば、コンソールに
Started 〇〇〇〇 in xx.xx seconds (~~~~)
というような表示が出ると思います。うまくいったら、このまま走らせておいてください。
   
![スクリーンショット 2020-05-18 22.55.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271486/069f0faf-d71b-b158-8e53-459e63a28b21.png)
  
次に、Postmanを使って確認します
。使い方は下を参考にしてください。
https://www.xlsoft.com/jp/blog/blog/2017/06/23/post-1638/

まずは、http://localhost:8080/api/v1/contents に対してGetリクエストを行います。
すると、当然ですが何も登録されていないのでからのリストが返ってくると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271486/bbbf16ea-7f13-2b14-abbc-f35550e61834.png)

次に、以下のコンテンツを http://localhost:8080/api/v1/contents にPostしてみてください。

```JSON
{
    "name": "content_1",
    "type": "English/reading"
}
```

すると、以下のようなレスポンスが返ってくると思います。

![スクリーンショット 2020-05-18 23.09.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271486/8b8dee67-c577-97f4-3bb5-28cd23b242c4.png)

同様に、もうひとつ追加してみましょう。

```JSON
{
    "name": "content_2",
    "type": "English/listening"
}
```

これで、２つのコンテンツがデータベースに登録されているはずです。
もう一度 http://localhost:8080/api/v1/contents に対してGetリクエストを行ってみましょう。
すると、以下のようにきちんと２つ登録されていることがわかります。

![スクリーンショット 2020-05-18 23.15.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271486/bf0c8ebd-0364-8a35-427f-24c5055da228.png)

同様に、Recordも記録してみましょう。
今度は http://localhost:8080/api/v1/records に以下のデータをPostしてみましょう。
この時、contentの中の要素は先ほどデータベースに登録したものと一致するようにしましょう(特にidのズレに注意)

```JSON
{
    "content": {
        "content_id": 1,
        "name": "content_1",
        "type": "English/reading"
    },
    "study_hours": 1.0
}
```

すると、以下のように登録されているはずです。

![スクリーンショット 2020-05-18 23.22.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271486/bb538f59-3b1c-ef9c-e279-65fd26cc1fbe.png)

同様に、study_hoursを2.5, 3.0としてそれぞれもう一度Postしてみましょう。
その後、 http://localhost:8080/api/v1/records に対してGetリクエストを行ってみましょう。
きちんと登録されていることを確認してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271486/99004459-1a73-05ab-2158-e2a6075d9153.png)

そうしたら、最後にデータを消去できるか確認してみましょう。
今回は真ん中の、record_id=5のデータを消してみます。
http://localhost:8080/api/v1/records/5 に対してDeleteリクエストを送ってみましょう。
Deleteメソッドはvoidで定義したので、成功すればなにも返ってこないと思います。試しにもう一度Deleteリクエストを同じURLに対して実行してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271486/60eab0b4-e215-7cda-2dbf-b14a7da4087a.png)

すると、上のようにエラーレスポンスが届くことが確認できます。
これはもう削除対象のデータがないため投げられたエラーで、レスポンスはSpringがいい感じに作ってくれています。さすが。

では、本当に削除できたのか確認しましょう。
http://localhost:8080/api/v1/records に対してGetリクエストをもう一度送りましょう。

![スクリーンショット 2020-05-18 23.30.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271486/35a37ea5-bfc7-5b32-013c-a91cd728dd2a.png)

確かにid=5のデータが削除されています。今回は行いませんが、contentsに対しても同様に調べることができます。
これでサービスが完成しました！
フロントエンドでUIを作ってあげれば、勉強記録サービスを作ることができるでしょう。