Angularチュートリアル + SpringBoot + PostgreSQLをやってみた

# Angular + SpringBoot + PostgreSQL

## はじめに
フロントエンド: Angular(TypeScript)
バックエンド: SpringBoot(Java)
DB: PostgreSQL

こちらの記事を参考にして、タイトル通りのものを作ってみました。
[Angularチュートリアル + Spring Bootやってみた](https://qiita.com/panichi/items/7e17820d02fa274bf04a)



## 前提条件

[Spring Initializer](https://start.spring.io/)でプロジェクトを作成していること
Maven Projectで作成し、
Dependenciesは`postgreSQL Driver`と`Lombok`を入れました。

名前は適当に


環境
- Mac OS Catalina 10.15.2
- Intellij Community 2019.3.1
- Java 11
- PostgreSQL 12.1

この記事ではMacでやっていますが、Windowsでも問題なく動きます。(7は確認済みだが、10は知らない)

``` zsh
% sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.2
BuildVersion:   19C57

% osascript -e 'version of app "IntelliJ IDEA"'   
2019.3.1

% java --version
java 11.0.4 2019-07-16 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.4+10-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.4+10-LTS, mixed mode)

 % postgres --version
postgres (PostgreSQL) 12.1

% ng --version
Angular CLI: 8.3.4
Node: 11.12.0
OS: darwin x64
Angular: 
... 

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.803.4
@angular-devkit/core         8.3.4
@angular-devkit/schematics   8.3.4
@schematics/angular          8.3.4
@schematics/update           0.803.4
rxjs                         6.4.0

```


## PostgreSQL
すでにPostgreSQLをインストールしている前提で進めます。
今回はスキーマを使ってやってます。

### スキーマ作成&テーブル作成
``` sql
CREATE SCHEMA tutorial
CREATE TABLE tutorial.heroes(
    id INT,
    name VARCHAR(64)
)
```

### モックデータ挿入

``` sql
INSERT INTO tutorial.heroes VALUES
(1, 'キャプテン・アメリカ'),
(2, 'アイアンマン'),
(3, 'ハルク'),
(4, 'ソー・オーディンソン'),
(5, 'ブラック・ウィドー'),
(6, 'ホークアイ'),
(7, 'ウィジョン'),
(8, 'スカーレット・ウィッチ'),
(9, 'ファルコン'),
(10, 'ウォーマシン'),
(11, 'キャプテン・マーベル');
```

これでPostgreSQLの準備はできました。

## Angular
[Angular チュートリアル](https://angular.jp/tutorial)がすべて終わっている前提で進めます。


### サービスクラス
サービスクラスのheroUrlをREST APIのURLに変更します。

``` hero.service.ts
...
// 略

export class HeroService {
// private heroesUrl = 'api/heroes';  // Web APIのURL
  private heroesUrl = 'http://localhost:8080'; // <= ここを追加

  httpOptions = {
    headers: new HttpHeaders({ "Content-Type": "application/json" })
  };

  constructor(
    private http: HttpClient,
    private messageService: MessageService
  ) {}

// 略
...
```

### app.module.ts

モックではなく、実際にAPIを叩いてデータを貰うため、
APIサーバっぽく振る舞ってくれるHttpClientInMemoryWebApiModuleをコメントアウトします。

``` app.module.ts
@NgModule({
  declarations: [
    AppComponent,
    HeroesComponent,
    HeroDetailComponent,
    MessagesComponent,
    DashboardComponent,
    HeroSearchComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule,
    HttpClientModule
    // HttpClientInMemoryWebApiModule.forRoot(
    //   InMemoryDataService, { dataEncapsulation: false }
    // )
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

```

### ヒーローコンポーネント

``` heroes.component.ts
add(name: string): void {
    name = name.trim();
    // ↓ 最後の要素に1プラスしたものを新ヒーローのIDとして追加
    let id = this.heroes.slice(-1)[0].id + 1;
    if (!name) { return; }
    // idを追加し、Heroとして引数を渡す
    this.heroService.addHero({ name, id } as Hero)
      .subscribe(hero => {
        this.heroes.push(hero);
      });
  }
```

Angularの変更はこれで終了です。

## バックエンド側 (Java)
### Hero class
ヒーローの型定義のため、ヒーロークラスを作成します。
Lombokを使っているのでsetter, getterはいりません。
(IntellijではPluginでlombokを入れる必要があります)

``` Hero.java
package tutorial.tutorial.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@AllArgsConstructor
@Data
@NoArgsConstructor
@ToString
public class Hero {
    private Integer id;
    private String name;
}
```

### HeroDAO class
データベースにアクセスするためのDAOクラスを作成します。
CRUD処理を全部作ります。


``` HeroDAO.java 
package tutorial.tutorial.model;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class HeroDAO {

    /**
     * DB接続情報を確立するメソッド
     *
     * @return conn コネクション情報
     */
    public Connection getConnection() throws ClassNotFoundException, SQLException {
        // Connection情報を格納するための変数を用意
        Connection conn = null;
        // 初期化
        Class.forName("org.postgresql.Driver");
        // DB情報を入力
        conn = DriverManager.getConnection("jdbc:postgresql://localhost/postgres?currentSchema=tutorial", "postgres", "postgres");
        // 自動コミットは無効にする
        conn.setAutoCommit(false);
        // コネクション情報を返す
        return conn;
    }

    /**
     * すべてのHeroを取得し返却するメソッド
     *
     * @return heroes Heroの型リスト
     */
    public List<Hero> findAll() throws ClassNotFoundException, SQLException {
        // Connection情報を格納するための変数用意
        Connection conn = null;
        // dtoクラスのインスタンス格納用
        List<Hero> heroes = new ArrayList<>();


        // データベースへの接続
        try {
            conn = getConnection();
            // SQL文を実行するためのオブジェクト生成
            Statement pstmt = conn.createStatement();
            // SELECT文の発行
            String sql = "SELECT * FROM tutorial.heroes";
            // SQL文の実行結果を取得（DBから受け取る値）
            ResultSet rs = pstmt.executeQuery(sql);

            // DBから受け取った値をレコード分だけ繰り返す
            while (rs.next()) {
                // Hero(DTO)クラスのインスタンスを生成
                Hero dto = new Hero();
                // カラムidの値をセット
                dto.setId(rs.getInt("id"));
                // カラムnameの値をセット
                dto.setName(rs.getString("name"));
                // インスタンスをListに格納
                heroes.add(dto);
                // while文で次のレコード処理へ(あれば)
            }
            //エラーキャッチ文
        } catch (SQLException e) {
            e.printStackTrace();
            // 例外の発生有無に関わらず実行する処理
        } finally {
            // もしconnの中身が入っていればdb接続を切る
            if (conn != null) {
                conn.close();
            }
        }
        // DTOクラスのインスタンスのListを返す
        return heroes;
    }


    /**
     * 引数で受け取るidに一致するHeroを取得し、返却するメソッド
     *
     * @param id
     * @return selectedHero
     */
    public Hero findOneHero(int id) throws ClassNotFoundException, SQLException {
        Connection conn = null;
        Hero selectedHero = new Hero();

        // データベースへの接続
        try {
            conn = getConnection();
            // SELECT文の発行
            String sql = "SELECT * FROM tutorial.heroes WHERE id = ?";

            // SQL文を実行するためのオブジェクト生成
            PreparedStatement pstmt = conn.prepareStatement(sql);

            // プレースホルダーで引数で受け取ったidをセットする。
            pstmt.setInt(1, id);

            // SQL文の実行結果を取得（DBから受け取る値）
            ResultSet rs = pstmt.executeQuery();

            // DBから受け取った値をdtoにセットする。
            while (rs.next()) {
                // Hero(DTO)クラスのインスタンスを生成
                Hero dto = new Hero();
                // カラムidの値をセット
                dto.setId(rs.getInt("id"));
                // カラムnameの値をセット
                dto.setName(rs.getString("name"));
                //
                selectedHero = dto;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return selectedHero;
    }

    /**
     * 引数で受け取るidに一致するHeroをUpdateするメソッド
     *
     * @param hero
     */
    public void updateHero(Hero hero) throws ClassNotFoundException, SQLException {
        Connection conn = null;
        try {
            conn = getConnection();
            String sql = "UPDATE tutorial.heroes SET name = ? WHERE id = ?";
            PreparedStatement pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, hero.getName());
            pstmt.setInt(2, hero.getId());
            pstmt.executeUpdate();
            conn.commit();

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
    }

    /**
     * 引数で受け取るidに一致するHeroを削除するメソッド
     *
     * @param id 消したいheroのid
     */
    public void deleteHero(int id) throws ClassNotFoundException, SQLException {
        Connection conn = null;
        try {
            conn = getConnection();
            String sql = "DELETE FROM tutorial.heroes WHERE id = ?";
            PreparedStatement pstmt = conn.prepareStatement(sql);
            pstmt.setInt(1, id);
            pstmt.executeUpdate();
            conn.commit();

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
    }

    /**
     * 引数で受け取るidとnameで新しいHeroをINSERTするメソッド
     *
     * @param hero
     */
    public void createHero(Hero hero) throws ClassNotFoundException, SQLException {
        Connection conn = null;
        try {
            conn = getConnection();
            String sql = "INSERT INTO tutorial.heroes VALUES(?, ?)";

            // SQL文を実行するためのオブジェクト生成
            PreparedStatement pstmt = conn.prepareStatement(sql);

            // プレースホルダーで引数で受け取ったidをセットする。
            pstmt.setInt(1, hero.getId());
            pstmt.setString(2, hero.getName());

            // SQL文の実行結果を取得（DBから受け取る値）
            pstmt.executeUpdate();
            conn.commit();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
    }
}

```

### HeroController class

REST APIを作ります。
以下の５つを作成しました。
- すべてのヒーローを返す getHeroes
- idに紐づくヒーローを返却する getHero
- 新しいヒーローを作成する create
- ヒーローを削除する delete
- ヒーローの情報を更新する update

チュートリアルに合わせて受け取るデータをHeroにしています。


``` HeroContoller.java
package tutorial.tutorial.controller;

import org.springframework.web.bind.annotation.*;
import tutorial.tutorial.model.*;
import java.sql.SQLException;
import java.util.*;

@RestController
public class HeroController {

    /**
     * DAOクラスからすべてのheroを受け取るメソッド
     *
     * @return heroList 
     */
    @GetMapping("/")
    public List<Hero> getHeroes() throws SQLException, ClassNotFoundException {
        HeroDAO dao = new HeroDAO();
        List<Hero> heroes = dao.findAll();
        List<Hero> heroList = new ArrayList<>();
        heroList.addAll(heroes);
        return heroList;
    }

    /**
     * DAOクラスからidに紐付くheroを受け取るメソッド
     *
     * @param id
     * @return hero
     */
    @GetMapping("/{id}")
    public Hero getHero(@PathVariable Integer id) throws SQLException, ClassNotFoundException {
        HeroDAO dao = new HeroDAO();
        Hero hero = dao.findOneHero(id);
        return hero;
    }

    /**
     * DAOクラスで、受け取ったidとnameでINSERTするメソッド
     *
     * @param newHero
     * @return hero
     */
    @PostMapping("/")
    public Hero create(@RequestBody Hero newHero) throws SQLException, ClassNotFoundException {
        HeroDAO dao = new HeroDAO();
        dao.createHero(newHero);
        return newHero;
    }


    /**
     * DAOクラスでidに紐付くheroをDELETEするメソッド
     *
     * @param id
     */
    @DeleteMapping("/{id}")
    public void delete(@PathVariable Integer id) throws SQLException, ClassNotFoundException {
        HeroDAO dao = new HeroDAO();
        dao.deleteHero(id);
    }

    /**
     * DAOクラスでidに紐付くheroをUPDATEするメソッド
     *
     * @param updatedHero
     */
    @PutMapping("/")
    public Hero update(@RequestBody Hero updatedHero) throws SQLException, ClassNotFoundException {
        HeroDAO dao = new HeroDAO();
        dao.updateHero(updatedHero);
        return updatedHero;
    }

}

```

## CORS対応
ajax等は、セキュリティのため、SOPによって、同一オリジンからしかリソースを取得することができません。
今回はAngularとSpringBootでポート番号が違うため、信頼できるオリジン間に限定してSOPを解除するためCORSを有効にしてあげる必要があります。



``` WebConfig.java
package tutorial.tutorial.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer{
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/*")
                // 許可するport番号を指定(Angular側)
                .allowedOrigins("http://localhost:3000")
                // 許可するメソッド一覧
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 許可するヘッダー
                .allowedHeaders("Origin", "X-Requested-With", "Content-Type", "Accept")
                .allowCredentials(false).maxAge(3600);
    }
}

```


## 実行する
TutorialApplicationとAngularを実行してみましょう。
APIが呼ばれて、DBが操作されていることが確認できます。

## まとめ
Webアプリがどういう風になっているのか、理解するために、普段使っているIonicと関係が深いAngularをフロントエンド、SpringBootをバックエンドに、そしてデータベースにPostgreSQLを使ったやり取りを作ってみました。