1
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-09

Angular + SpringBoot + PostgreSQL

はじめに

フロントエンド: Angular(TypeScript)
バックエンド: SpringBoot(Java)
DB: PostgreSQL

こちらの記事を参考にして、タイトル通りのものを作ってみました。
Angularチュートリアル + Spring Bootやってみた

前提条件

Spring Initializerでプロジェクトを作成していること
Maven Projectで作成し、
DependenciesはpostgreSQL DriverLombokを入れました。

名前は適当に

環境

  • Mac OS Catalina 10.15.2
  • Intellij Community 2019.3.1
  • Java 11
  • PostgreSQL 12.1

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

% 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をインストールしている前提で進めます。
今回はスキーマを使ってやってます。

スキーマ作成&テーブル作成

CREATE SCHEMA tutorial
CREATE TABLE tutorial.heroes(
    id INT,
    name VARCHAR(64)
)

モックデータ挿入

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

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

Angular

Angular チュートリアルがすべて終わっている前提で進めます。

サービスクラス

サービスクラスの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を作ります。
以下の5つを作成しました。

  • すべてのヒーローを返す 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を使ったやり取りを作ってみました。

1
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?