Nuxt 3+Spring BootでREST API #5 flywayでDBのマイグレーションを行う
目次
flywayのインストール
前回で無事にマルチプロジェクト構成にできたので、今回はマイグレーション周りを整備していく。
build.gradle
に組み込む方法もあるっぽいけど今回はnpmでnode-flywaydb
を導入する。
今回はひとまずローカルで実行できるところまで。
まずはnpm init
→npm install node-flywaydb
でインストールを行う。
設定ファイルの記述
以下を参考に config.js
を記述する。
github.com
// This can be a function or an object literal. module.exports = function () { console.log(`migaration start`); return { flywayArgs: { url: `jdbc:mysql://localhost:3307/base`, schemas: "base", locations: "filesystem:migrate/sql", user: "root", password: ****, sqlMigrationSuffixes: ".sql", baselineOnMigrate: true, }, // Use to configure environment variables used by flyway env: { JAVA_ARGS: "-Djava.util.logging.config.file=./conf/logging.properties", }, version: "6.3.2", // optional, empty or missing will download the latest mavenPlugins: [ { // optional, use to add any plugins (ie. logging) groupId: "org.slf4j", artifactId: "slf4j-api", version: "1.7.25", // This can be a specifc url to download that may be different then the auto generated url. downloadUrl: "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar", }, { groupId: "org.slf4j", artifactId: "slf4j-jdk14", version: "1.7.25", }, ], downloads: { storageDirectory: "migrate/lib", // optional, the specific directory to store the flyway downloaded files. The directory must be writable by the node app process' user. expirationTimeInMs: -1, // optional, -1 will never check for updates, defaults to 1 day. }, }; };
package.jsonの編集
npmコマンドでマイグレーションを実行できるようにする。
scripts が以下になるように2行追記。
"scripts": { "migrate": "flyway -c migrate/config.js migrate", "clean": "flyway -c migrate/config.js clean", "test": "echo \"Error: no test specified\" && exit 1" },
npm run migrate
を実行してみる。
config.jsで指定したbase
というスキーマができていれば成功。
マイグレーションファイルの作成
migration直下に sql
ディレクトリを作成し、ここにマイグレーションファイルを配置していくことにする。
今回は V1.0.0__init.sql
という名前で作成した。flywayのマイグレーションファイルには命名規則があり、正常に動作させるには厳密に従う必要があるので注意すること。(ここでは詳述しない)
以下、特に意味のないテスト用テーブルのDDL。
DROP TABLE IF EXISTS t_test; CREATE TABLE t_test ( id varchar(8) not null comment 'ID', name varchar(20) not null comment '名称', constraint t_test_pk primary key (id) ) comment='テスト';
試運転
再度 npm run migrate
を実行してみる。
テーブルが作成された。
先ほどpackage.jsonに定義した、マイグレーションの進行状況をクリーンするためのコマンドも動作確認しておく。
npm run clean
を実行。
テーブルが削除されていればOK。
参考
Nuxt 3+Spring BootでREST API #4 マルチプロジェクト構成にする
目次
- Gradleのインストール
- プロジェクト構成
- ルートプロジェクトの作成
- ルートプロジェクトの作成
- Gradle Wrapperの導入 & ビルド
Gradleのインストール
Gradleコマンドを使えるようにする。Gradle Releasesにアクセス。
Downloadからcompleteを選択してダウンロード。
zipファイルを解凍して任意のパスに配置する。
以下の環境変数を登録する。
- GRADLE_HOME
- 値: gradleのパス
- PATH
- 値: %GRADLE_HOME%\bin
gradle -v
でパスが通っているか確認する。
プロジェクト構成
現状APIのプロジェクトを作成しただけなので、ルートディレクトリを切って、API、フロント、マイグレーションなどの単位で分割されたマルチプロジェクト構成にしていく。
構成は以下のようなイメージ。
.(root) ├─ api (APIサーバのプロジェクト) ├─ common (共通ライブラリ) ├─ iac (IaCのプロジェクト) ├─ migrate (マイグレーション用のプロジェクト) ├─ repository (DAOなどDBアクセス用のプロジェクト) ├─ ui (フロントのプロジェクト) ├─ build.gradle └─ setting.gradle
ルートプロジェクトの作成
gradle init
を実行。
ルートパスに作成されたsettings.gradleを編集する。
/* * This file was generated by the Gradle 'init' task. * * The settings file is used to specify which projects to include in your build. * * Detailed information about configuring a multi-project build in Gradle can be found * in the user manual at https://docs.gradle.org/6.9.3/userguide/multi_project_builds.html */ rootProject.name = 'rest-api-sample' include 'api', 'common', 'repository'
これでapi
, common
, repository
がサブプロジェクトとして認識されるようになる。
Gradle Wrapperの導入 & ビルド
Gradleの実行環境がなくても (gradleコマンドなしで) ビルドできるようにするために、Gradle Wrapper を導入する。
gradle wrapper
を実行。
ここでビルドが通らず FAILURE: Build failed with an exception.
と怒られ、以下のエラーが出た。
* What went wrong: Could not compile settings file 'C:\work\repos\*******\rest-api-sample\settings.gradle'. > startup failed: General error during semantic analysis: Unsupported class file major version 61
ひとまずgradle wrapper周りのライブラリは追加されたので、ビルドできない原因を突き止める。
エラー内容でググったところ、Gradle 6系だとJava16以降のバージョンをサポートしていないようなので、gradle-wrapper.properties
を以下のように修正した。
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
再度build実行したところ、無事成功。
参考
Windows10にGradleをインストール - Qiita
GradleでJavaのマルチプロジェクト作成 - Qiita
Nuxt 3+Spring BootでREST API #3 DBをDockerイメージとして起動する
目次
- Docker経由でMySQLイメージを取得する
- docker-compose.yml を記述する
- Dockerイメージを立ち上げて疎通確認
Docker経由でMySQLイメージを取得する
MySQLイメージを取得する。 Dockerを起動した状態で以下コマンドを実行。
$ docker pull mysql:latest
docker-compose.yml を記述する
設定内容は以下。
version: "3" services: db: image: mysql:5.7 container_name: mysql_sample environment: MYSQL_ROOT_PASSWORD: **** MYSQL_DATABASE: base MYSQL_USER: docker MYSQL_PASSWORD: **** TZ: "Asia/Tokyo" command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci volumes: - ./db/data:/var/lib/mysql - ./db/my.cnf:/etc/mysql/conf.d/my.cnf - ./db/sql:/docker-entrypoint-initdb.d ports: - 3307:3306
localhostで動作することを想定してportは3307を指定。
Dockerイメージを立ち上げて疎通確認
以下コマンドを実行。
docker-compose up -d db
A5M2で疎通確認。
Nuxt 3+Spring BootでREST API #2 HTTPリクエストを受け付けるコントローラを作成する
目次
コントローラクラスを作成する
com.sample.api
にcontroller
のパッケージを切って、以下のヘルスチェック用コントローラクラスを作成する。
package com.sample.api.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("healthcheck") public class HealthCheckController { @RequestMapping(method = RequestMethod.GET) public String healthCheck() { return "OK"; } }
ヘルスチェックはAWSにデプロイした際にALBからのヘルスチェックを受けるためにも使用する。(後述)
リクエストを返すことを確認する
Postmanから http://localhost:8080/healthcheck
にアクセスする
OKが返ってくればOK
Nuxt 3+Spring BootでREST API #1 APIプロジェクトを作成する
目次
- Spring Initializrでプロジェクトを作成する
- IntelliJの設定を行う
- プロジェクトを実行する
プロジェクトの雛形を作成する
Spring Initializr で雛形を作成する。
今回は以下の内容を設定した。
「GENERATE」を押下すると、プロジェクトのzipファイルがダウンロードされる
IntelliJの設定を行う
Gradle JVMの設定
アノテーションプロセッサを有効にする
コンパイル時にアノテーションに基づいてコードを検証、生成する仕組み(らしい)
SDKの設定
- プロジェクト構造 > プロジェクト
- SDKを選択
- 言語レベルがSpring Initializrで選択したものになっていることを確認
プロジェクトを実行して立ち上がればOK
参考
Nuxt 3+Spring BootでREST API #0 基本構成
イントロダクション
ここ数ヶ月、タイトルにあるような技術構成でWebアプリの開発をしている。
構築でいろいろと詰まったりしたので備忘録として作業ログを残していく。
やること
APIサーバ、フロントエンド、DBなど基盤構築の手順を記録する。
インフラ(AWS)、CI/CD(GitHub Actions)などにも触れる。
やらないこと
基盤構築周りの話がメインになるため、Nuxtのライフサイクルやら、テスト手法やら、個々の技術要素についての詳細には立ち入らない。
技術構成
以下のような感じ。
API
フロントエンド
- Nuxt 3
- テストはJest
RDB
インフラ
- フロントはS3においてCloudFrontで配信
- APIサーバはECSにデプロイ
- Cognitoで認証認可を行う
CI/CD
- GitHub Actionsで自動テスト+自動デプロイ
- CDKでプロビジョニングのコード化を行うところまでする
目次
適宜リンクは更新していく。
- #0 基本構成 ←本記事
- #1 APIプロジェクトを作成する
- #2 HTTPリクエストを受け付けるコントローラを作成する
- #3 DBをDockerイメージとして起動する
- #4 マルチプロジェクト構成にする
- #5 flywayでDBのマイグレーションを行う
- #6 JUnitでテストを書く
- #7 GitHub ActionsでCI環境を構築する
- #8 フロントプロジェクトを作成する
- #9 NuxtからHTTPリクエストを送る
- #10 Jestでテストを書く
- #11 フロントのテストを自動化する
- #12 フロントのインフラを構築する(手動編)
- #13 フロントのインフラを構築する(CloudFormation編)
- #14 フロントのインフラを構築する(AWS CDK編)
- #15 APIサーバのインフラを構築する(手動編)
- #16 APIサーバのインフラを構築する(CloudFormation編)
- #17 APIサーバのインフラを構築する(AWS CDK編)
まずはAPIプロジェクトを作成するところから。
[C#] 型引数を変えながらジェネリクスメソッドをループ処理で実行する
メモ。
経緯
引数が指定した型に変換可能であるかを判定して結果を返すジェネリックメソッド CanParse<T>
を作った。
シグネチャは以下の通り。
public static bool CanParse<T>(string value) where T : IConvertible, new() { // 判定処理(省略) }
このメソッドの動きを確認するため、
適当な値が入った以下のようなリストを用意した。
var values = new List<string>() { "-129", "-128", "-1", "0", "127", "128", "255", "256", "65535", "65536", "-32769", "-32768", "32767", "32768", "-2147483649", "-2147483648", "2147483647", "2147483648" };
このリスト内の全ての値を、全てのプリミティブな数値型(byte
, decimal
, double
, float
, int
, uint
, long
...)に対して総当たりで実行し、メソッドの動きを確認したかったのだがループ処理の実装で詰まってしまった。
// 型リスト var types = new List<Type>() { typeof(byte), typeof(decimal), typeof(double) // 以下略 }; // 総当たり foreach (var type in types) { foreach (var value in values) { // ここでコンパイルエラー Console.WriteLine(string.Format("{0}<{1}>: {2}\t({3})", nameof(CanParse), type, CanParse<type>(value), value ?? "null")); } }
CanParse<type>(value)
が問題。
型引数は変数として扱うことができないため、上記の書き方だとエラーとなってしまう。
対処法
デリゲートで実装することでループ処理できるようになった。
using System; using System.Collections.Generic; using System.Reflection; // delegate bool CanParseDelegate(string value); // 上のスコープで↑を宣言済 // 値リスト var values = new List<string>() { "-129", "-128", "-1", "0", "127", "128", "255", "256", "65535", "65536", "-32769", "-32768", "32767", "32768", "-2147483649", "-2147483648", "2147483647", "2147483648" }; // デリゲートリスト var delegates = new List<CanParseDelegate> { new CanParseDelegate(CanParse<bool>), new CanParseDelegate(CanParse<byte>), new CanParseDelegate(CanParse<sbyte>), new CanParseDelegate(CanParse<char>), new CanParseDelegate(CanParse<decimal>), new CanParseDelegate(CanParse<double>), new CanParseDelegate(CanParse<float>), new CanParseDelegate(CanParse<int>), new CanParseDelegate(CanParse<uint>), new CanParseDelegate(CanParse<long>), new CanParseDelegate(CanParse<ulong>), new CanParseDelegate(CanParse<short>), new CanParseDelegate(CanParse<ushort>) }; foreach (var dlg in delegates) { foreach (var value in values) { // 型引数取得 var arg = dlg.Method.GetGenericArguments(); Console.WriteLine(string.Format("{0}<{1}>: {2}\t({3})", nameof(CanParse), arg[0], dlg(value), value ?? "null")); } }
まとめ
- 型引数は変数として扱うことができないため、ループ処理する際にはデリゲートを用意する等の工夫が必要。
- リフレクションでごり押しする方法もあるらしい。(試していない)
GetGenericArguments()
で型引数が取得できる。Type
の配列として返されるため、参照する際は添字が必要な点に注意。