Spring bootとthymeleafでPostgresにINSERT, SELECTをするまで

## やったこと
spring boot勉強中の途中経過まとめ。
[前回書いた記事](https://qiita.com/PG-practice/items/82eca81898a415d658cd)で、spring bootでHello Worldするところまでやった。結局ローカルで作業しているが。今回やったことは以下。

- thymeleafで入力フォームを作った
- 画面の入力データを使ってpostgres DBにINSERT、SELECTのSQLを発行した
- 結果を表示する

コードやファイルごとにコメントを書く形式にした。あまりまとまっていないから後で見返すの大変そう...。

## できたもの
### コード
https://github.com/PG-practice/lyricWall/tree/simple_ui_connect_db
### 画面遷移図
![spring画面遷移.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490290/64b53247-1eca-9b4d-e56e-84891490463d.png)

### 擬コラボレーション図
![Spring20200503構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490290/aa6ab3a6-1541-de72-4150-89734ace5369.png)


## 学んだこと
- spring bootのDI (Dependency Injection)のされ方。シングルトン。
- spring bootでDB接続して結果を表示するまでの流れ
- いくつかのアノテーション
- Formオブジェクトとth:objectのバインドのされ方
- th:objectやth:text自体はnullでもいいけど、プロパティにアクセスしようとするとエラーになるから空インスタンスを`model`に詰めてhtml返却する
- 二重送信防止策の一つ、PRG (POST-REDIRECT-GET)
- spring jdbc + prepared statementによる複数INSERTは遅そう?


## コード以外の準備
### テーブル作成
以下のSQLでテーブルを手動で生成した。

```sql
CREATE TABLE IF NOT EXISTS lyricTable (
    artist VARCHAR(32),
    title VARCHAR(32),
    words_writer VARCHAR(16),
    music_composer VARCHAR(16),
    lyric VARCHAR(1024),
    url VARCHAR(256),
    PRIMARY KEY (artist,title)
);
```
### postgres接続用設定
#### 依存関係追加
[前回書いた記事](https://qiita.com/PG-practice/items/82eca81898a415d658cd)時点ではpostgresqlがあればいいと思っていたが、jdbcが必要らしいので以下を追加。

```Groovy:build.gradle
dependencies {
         //省略
	compile('org.springframework.boot:spring-boot-starter-jdbc')
}
```
#### 接続情報記述
application.propatiesはyamlにしてもいいようなのでyamlにした。urlの最後のDB名は必須。

```yaml:application.yaml
spring:
 datasource:
    driver-class-name: "org.postgresql.Driver"
    # URLの最後のDB名は必須
    url: "jdbc:postgresql://ubuntu:5432/postgres"
    username: "postgres"
    password: "" 
```
## 各コードのメモ
後で見返せるように細かくコメント。

### Lyric.java
```java:Lyric.java
public class Lyric {
    private String artist;
    private String title;
    private String words_writer;
    private String music_composer;
    private String lyric;
    private String url;
    
    //セッター、ゲッター、引数のないコンストラクタ
    //(省略)
}

```
- セッター、ゲッター、引数のないコンストラクタがないとエラー出た気がする
- `@Entity`はつけなかった(DIしないからかな？)

### LyricDaoImpl.java
interfaceのコードは省略。実装のみ。

```java:LyricDaoImpl.java
//import文多数

@Repository
public class LyricDaoImpl implements LyricDao {
    // DIするためのテンプレ。newせずにAutoWiredで呼び出すだけでよい。
    private final JdbcTemplate jdbcTemplate;
    //上とセットで必要な時にDIでインスタンスが参照される
    @Autowired
    public LyricDaoImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    //指定歌手の曲情報を取得
    @Override
    public List<Lyric> getSongs(String artistName) {
        // preparedStatementがSQLインジェクション対策になるので書き換えたいが、今回はこのまま。
        String sql = "SELECT * FROM lyricTable WHERE artist='" + artistName + "';";
        List<Map<String, Object>> resultList = jdbcTemplate.queryForList(sql);

        List<Lyric> list = new ArrayList<Lyric>();
        for(Map<String, Object> result:resultList){
            Lyric lyric = new Lyric();
            lyric.setArtist((String)result.get("artist"));
            lyric.setTitle((String)result.get("title"));
            lyric.setWords_writer((String)result.get("words_writer"));
            lyric.setMusic_composer((String)result.get("music_composer"));
            lyric.setLyric((String)result.get("lyric"));
            lyric.setUrl((String)result.get("url"));
            list.add(lyric);
        }
        return list;
    }

    //リストの曲情報を複数INSERT。
    @Override
    public void insertSongs(List<Lyric> list) {
        String sql = "INSERT INTO lyricTable VALUES (?,?,?,?,?,?)";
        //preparedstatement複数INSERT。テンプレとしてこのまま使った。これでもう動く。
        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter(){
            
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                Lyric lyric = list.get(i);
                ps.setString(1, lyric.getArtist());
                ps.setString(2, lyric.getTitle());
                ps.setString(3, lyric.getWords_writer());
                ps.setString(4, lyric.getMusic_composer());
                ps.setString(5, lyric.getLyric());
                ps.setString(6, lyric.getUrl());
            }
        
            @Override
            public int getBatchSize() {
                return list.size();
            }
        });

    }

    
}

```
- `insertSongs()`は複数INSERTできるようにしたが、画面からは1曲しか来ない。
- `batchUpdate`はbatch sizeごとにクエリをまとめて送ってくれるらしいが、コネクションがまとめられるだけで結局1行ずつINSERTするため遅い。設定すれば一括INSERTもできるらしい。

### LyricServiceImpl.java
interfaceのコードは省略。実装のみ。

```java:LyricServiceImple.java
//数多のimport文
@Service
public class LyricServiceImpl implements LyricService {
    // DI用テンプレ。
    private final LyricDao dao;

    @Autowired
    public LyricServiceImpl(LyricDao dao) {
        //実装クラスのインスタンスがDIされる
        this.dao = dao;
    }

    @Override
    public List<Lyric> getSongs(String artistName) {
        // TODO Auto-generated method stub
        return dao.getSongs(artistName);
    }

    @Override
    public void insertSongs(List<Lyric> list) {
        // TODO Auto-generated method stub
        dao.insertSongs(list);
    }

}
```
- DI用のコンストラクタの引数はinterface型。それで勝手に実装クラスのインスタンスをDIしてくれるよう。まだあまりわかっていない。今のところは、シングルトンでいいオブジェクトはnewしないということで覚えておく(要確認)。
    - interface名しか書かないため、実装するクラス名が変更されてもこちらは書き換えずに済む
    - 実装するクラスができていなくてもエラーが出ない？？？(未確認)

### WebMvcControllerAdvice.java
Controllerの前に共通して処理されるメソッドを定義

```java:WebMvcControllerAdvice.java
@ControllerAdvice
public class WebMvcControllerAdvice {

    //htmlから送信された空文字をnullとして扱うらしい
    @InitBinder
    public void initBinder(WebDataBinder dataBinder) {
        dataBinder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
    }

    //DBエラーが起こった場合にはエラーメッセージと、空のFormオブジェクトをmodelに入れてhtml返却
    @ExceptionHandler(PSQLException.class)
    public String sqlException(PSQLException e, Model model){
        model.addAttribute("errorMessage",e.getMessage());
        model.addAttribute("searchForm", new SearchForm());
        model.addAttribute("insertForm", new InsertForm());
        return "form.html";
    }
   
}
```
- 空のFormオブジェクトを`model`に入れている。htmlでプロパティにアクセスしようとしているため、オブジェクトがないとエラーになる(後述)。ベストプラクティスはどうすればいいんだ。

### SearchForm.java
selectボタンを含むformタグにある`th:object="${searchForm}"`にバインドされるつもりの型。変数名はhtmlとcontroller内のメソッドの引数を一致させること。

```java:SearchForm.java
//importなど少々

public class SearchForm {
    @NotNull(message="歌手名を入力してください")
    private String artistName;

   //セッター、ゲッター、コンストラクタ
}
```

### InsertForm.java
insertボタンを含むformタグにある`th:object="${searchForm}"`にバインドされるつもりの型。変数名はhtmlとcontroller内のメソッドの引数を一致させること。

```java:InsertForm.java
public class InsertForm {
//import文など少々
    @NotNull(message="登録には歌手名の入力は必須です")
    private String artistName;

    @NotNull(message = "登録には曲名の入力は必須です")
    private String title;

    private String wordsWriter;
    private String musicComposer;
    private String lyric;
    private String url;

   //セッター、ゲッター、コンストラクタ
}
```

### LyricController.java

```java:LyricController.java
//import文など

//ローカルの場合"http://localhost:8080/lyric"に来たリクエストがこのクラス内部にマッピング
@Controller
@RequestMapping("/lyric")
public class LyricController {
    //newしないでDI
    private final LyricService lyricService;

    @Autowired
    public LyricController(LyricService lyricService) {
        this.lyricService = lyricService;
    }
    //modelのキーに"insertCompleteMessage"があればそのまま使用、なければadd
    @GetMapping("/form")
    public String form(Model model, @ModelAttribute("insertCompleteMessage") String message){
        model.addAttribute("title", "artist name");
        //templateフォルダ内の該当ファイルを返す
        return "form.html";
    }
    //SearchFormクラスのvalidation anotationに引っかかるとresultにエラーが入る
    @GetMapping("/select")
    public String select(@Validated SearchForm searchForm, BindingResult result, Model model){
        if(result.hasErrors()){
            model.addAttribute("title", "Error Page");
            return "form.html";
        }
        List<Lyric> list = lyricService.getSongs(searchForm.getArtistName());
        if(list.size()!=0){
            model.addAttribute("lyricList", list);
        }else{
            model.addAttribute("noResultMessage", "該当アーティストの曲が登録されていない");
        }
        return "list.html";
    }
    //PRGのためにリダイレクトとフラッシュスコープ(リクエスト1回終わると消える属性)を使用
    @PostMapping("/insert")
    public String insert(@Validated InsertForm insertForm, BindingResult result, Model model, RedirectAttributes redirectAttributes){
        if(result.hasErrors()){
            model.addAttribute("title", "Error Page");
            return "form.html";
        }
        // まずは入力フォームは一つ。DAOはリスト形式のため、要素が1つのリストにする
        List<Lyric> list = convertInsertForm(insertForm);
        // TDDO エラーがあった場合の処理を書くこと
        lyricService.insertSongs(list);
        //@GetMapping("/form")にリダイレクト。フラッシュスコープ設定。
        redirectAttributes.addFlashAttribute("insertCompleteMessage", "insert complete");
        return "redirect:form";
    }

    //上記の引数にSearchFormが入ってなくても勝手にmodelに入れてくれる。メソッド名は何でもいい。
    @ModelAttribute
    SearchForm setSearchForm(){
        return new SearchForm();
    }    

    @ModelAttribute
    InsertForm setInsertForm(){
        return new InsertForm();
    }

    //InseartFormからLyricへ変換
    public static List<Lyric> convertInsertForm(InsertForm insertForm){
        List<Lyric> list = new ArrayList<>();
        Lyric lyric = new Lyric();
        lyric.setArtist(insertForm.getArtistName());
        lyric.setTitle(insertForm.getTitle());
        lyric.setWords_writer(insertForm.getWordsWriter());
        lyric.setMusic_composer(insertForm.getMusicComposer());
        lyric.setLyric(insertForm.getLyric());
        lyric.setUrl(insertForm.getUrl());
        list.add(lyric);
        return list;
    }

}

```
-  引数の`BindingResult result`はFormオブジェクトの直後に置かないとアクセス時にwhitelabel errorになる。引数の順番が関係ある。

#### PRGとは
`@PostMapping`の最後のリダイレクトが二重送信対策のPRGと呼ばれるもの。

- 防止できる二重送信
    - INSERT後、ブラウザの更新による再送信
    - INSERT後に表示されたページで、そのままもう一度INSERTボタンを押すことによる再送信
- 防止できなさそうな二重送信
    - 頑張って高速で2回INSERTボタンを押す

最後を`return form.html`にしてリダイレクトなしでhtmlを返却すると、
1. `model`に属性が残ったまま → ブラウザではオブジェクトがバインドされたまま
2. 最終リクエストがPOSTになる → ブラウザ更新時にPOSTされる
ということになる。これを防ぐために
1. `model`のバインドを解除
2. 強制的にGETリクエストさせてhtmlを返却
をするのがPRG (POST-REDIRECT-GET)。

リダイレクト時は`model`のプロパティは引き継がれない(デバッガでリダイレクト時にmodelの中のFormオブジェクトが初期化されることは確認済み)。そのため、`redirectAttributes`に入った`insert complete`の文字以外は初期化された状態でレスポンスされる。さらに、`insert complete`のメッセージはフラッシュスコープ(1回のリクエストで消える)に入っているため、ブラウザを更新すると消える。

### list.html
`${プロパティ名}`でサーバサイドで`model`に付与されたキーに対応するオブジェクトが受け取れる。

```html:list.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeLeaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="${title}">Insert</title>
</head>
<body>
    <h1 th:text="${title}">title</h1>
    <p th:text="${noResultMessage}"></p>
    <table th:if="${lyricList}">
        <tr>
            <th>Artist Name</th><th>song title</th><th>lyric</th>
        </tr>
        <tr th:each="lyric:${lyricList}">
            <td th:text="${lyric.artist}"></td>
            <td th:text="${lyric.title}"></td>
            <td th:text="${lyric.lyric}"></td>
        </tr>
    </table>
</body>
```
### form.html
メインの画面。http://localhost:8080/lyric/formでアクセス。

```html:form.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeLeaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="${title}">Insert</title>
</head>
<body>
    <h1 th:text="${title}">title</h1>
    <h1 th:text="${redirectTitle}">title</h1>
    <form method="GET" action="#" th:action="@{/lyric/select}" th:object="${searchForm}">
        <label for="selArtistNameId">Artist Name:</label>
        <input id="selArtistNameId" name="artistName" type="text" th:value="*{artistName}">
        <div th:if="${#fields.hasErrors('artistName')}" th:errors="*{artistName}"></div>
        <input type="submit" value="search">
    </form>
    <h1>曲登録</h1>
    <form method="POST" action="#" th:action="@{/lyric/insert}" th:object="${insertForm}">
        <label for="insArtistNameId">Artist Name:</label>
        <input id="insArtistNameId" name="artistName" type="text" th:value="*{artistName}">
        <div th:if="${#fields.hasErrors('artistName')}" th:errors="*{artistName}"></div>
        <label for="title">Song Title:</label>
        <input id="titleId" name="title" type="text" th:value="*{title}">
        <div th:if="${#fields.hasErrors('title')}" th:errors="*{title}"></div>
        <label for="wordsWriter">作詞:</label>
        <input id="wordsWriterId" name="wordsWriter" type="text" th:value="*{wordsWriter}">
        <label for="musicComposer">作曲:</label>
        <input id="musicComposerId" name="musicComposer" type="text" th:value="*{musicComposer}">
        <label for="lyric">歌詞:</label>
        <input id="lyricId" name="lyric" type="text" th:value="*{lyric}">
        <label for="url">歌詞URL:</label>
        <input id="urlId" name="url" type="text" th:value="*{url}">
        <input type="submit" value="insert">
    </form>
    <p th:text="${insertCompleteMessage}"></p>
    <p th:if="${errorMessage}" th:text="'エラーメッセージ：'+${errorMessage}"></p>
</body>
</html>
```
- 例えば、`${insertForm}`オブジェクトは、送信する時点で`name=`の値と`Controller`内のマッピングメソッドの引数`InsertForm`のプロパティ名が紐づけられる。
    - nameとFormオブジェクトのプロパティ名が異なると拾ってもらえない
    - `SearchForm`を引数に入れてしまうと`artistName`のプロパティ名が同じため、こちらも同時に紐づけられてしまった。`@ModelAttribute`すると関係ないFormに勝手に値が紐づけられなかったのでよし。
- `model`に必要なオブジェクトを詰めて返さないと、`th:value="$*{artistName}"`とかで引っかかってwhitelabel errorになる。`WebMvcControllerAdvice.java`のエラーハンドラでわざわざインスタンス生成して`model`に入れているのはこのため

## 調べること
- DIのメリットと挙動
- `<div th:if="${#fields.hasErrors('title')}" th:errors="*{title}"></div>`
- POST後のURLに出てきたjsesessionidってなんだ

## 参考：
会社がudemy for Business に入れてくれたおかげでGWは色々あさっていますが、今回の記事は以下の講座を多分に参考にしています。内容は全然違いますが、一部そのままお借りしているメソッド等があります。https://www.udemy.com/course/java_spring_beginner/