Angular + SpringBoot + PostgreSQL
はじめに
フロントエンド: Angular(TypeScript)
バックエンド: SpringBoot(Java)
DB: PostgreSQL
こちらの記事を参考にして、タイトル通りのものを作ってみました。
Angularチュートリアル + Spring Bootやってみた
前提条件
Spring Initializerでプロジェクトを作成していること
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は知らない)
% 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に変更します。
...
// 略
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をコメントアウトします。
@NgModule({
declarations: [
AppComponent,
HeroesComponent,
HeroDetailComponent,
MessagesComponent,
DashboardComponent,
HeroSearchComponent
],
imports: [
BrowserModule,
FormsModule,
AppRoutingModule,
HttpClientModule
// HttpClientInMemoryWebApiModule.forRoot(
// InMemoryDataService, { dataEncapsulation: false }
// )
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
ヒーローコンポーネント
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を入れる必要があります)
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処理を全部作ります。
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にしています。
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を有効にしてあげる必要があります。
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を使ったやり取りを作ってみました。