Wikipedia日本語版のダンプデータを手元で復元する(できてない)
TL;DR
- 少し用事があって多数の日本語テキストを含むデータベースが欲しかった
- 公開データベースといえば Wikipedia だろうと思って、手順どおりにダンプデータの復元を試みた
- MySQL のインスタンスに mediawiki:latest をつなぐだけのはずだった
- インポートがいつまで経っても終わらなくて諦めた
- 試しに24時間放置したら25万ページくらいまでは進んだ
Wikipediaのダンプファイルのダウンロード
Wikipediaのダンプファイルのダウンロード速度は極めて低速で、普通にやると5時間くらいかかる
- ファイル配布サイトと、ファイルをダウンロードするPCのネットワークが遠いのが悪い
- ファイル配布サイトに近いところでダウンロードすればよい
- AWSネットワークは配布サイトに近いんじゃないかと予想
- AWS CloudShellで試したら予想通り高速だった
- S3からPCへダウンロードするのは十分高速なので、後はS3に配置できればよい
- AWS CloudShellには永続化ストレージの容量制限(1GB)がある
- のサービスクォータと制限AWS CloudShell - AWS CloudShell
- ダンプファイルは合計7.8GB
- 中間的なファイル置き場として使えない
- 分割するのは面倒でやりたくない
- よく見たらルートファイルシステムには 10GB くらい空きがある
/tar/tmp
や/tmp
などの場所が使えそう→使えた- 全体で10分くらいで済んだ
MediaWikiのセットアップウィザードはMySQLへSSLで接続できない
- PlanetScaleやAWS RDSでインスタンスを作成すると、最初はSSL接続が必須になっている(たぶん)
- しかし、MediaWikiのセットアップウィザードにはSSL接続を指定するところがなくて、どうしようもない
includes/libs/rdbms/database/DatabaseMysqli.php
の L150 周辺を編集して$flags |= MYSQLI_CLIENT_SSL;
とすれば一応進められる- セットアップした後は
LocalSettings.php
で$wgDBssl = true;
を指定できることになっている
ダンプファイルのインポート
とにかく遅い。履歴をスキップするとかのコツがありそう。
MySQLの実行環境 | 処理速度(ページ/秒) |
---|---|
Docker Desktop | 10 |
RDS(db.t3.micro) | 0.8 |
begin/endと名付けられたtypeglobを見た
こういう書き方ができるのを知らなかった。
*enable = *begin = \&import; *disable = *end = \&unimport;
- コードリファレンスをbegin enable end disableと名付けたtypeglobへ保存している
- レキシカルスコープでパッケージを有効化、無効化するのにenable/disableを使っていて分かりやすい
ただ、beginとendのtypeglobが分からなくて、もやもやする。
BEGINやENDブロックは分かるけど、ブロックとコードリファレンスは等価なんだっけ??
The Perl Playgroundでいくつか標準モジュールを眺めたけど、そういうシンボルは存在しない。本体の実装読まないと分からないのかもしれない。
use Benchmark; use Carp; use Time::HiRes; use Math::BigInt; foreach my $m (qw{Benchmark Carp Time::HiRes Math::BigInt}) { foreach my $entry (qw{BEGIN END DESTROY begin end import} ) { print $entry . "\n"; print join("\n", map { " $_ -- " . *{$m . "::" . $entry}{$_} } qw{ CODE IO GLOB FORMAT NAME PACKAGE } ) . "\n"; } }
BEGIN CODE -- IO -- GLOB -- GLOB(0x55a7b7367af8) FORMAT -- NAME -- BEGIN PACKAGE -- Benchmark END CODE -- IO -- GLOB -- GLOB(0x55a7b7345740) FORMAT -- NAME -- END PACKAGE -- Benchmark DESTROY CODE -- IO -- GLOB -- GLOB(0x55a7b73458d8) FORMAT -- NAME -- DESTROY PACKAGE -- Benchmark begin CODE -- IO -- GLOB -- GLOB(0x55a7b73671f8) FORMAT -- NAME -- begin PACKAGE -- Benchmark end CODE -- IO -- GLOB -- GLOB(0x55a7b7367210) FORMAT -- NAME -- end PACKAGE -- Benchmark import CODE -- CODE(0x55a7b7437350) IO -- GLOB -- GLOB(0x55a7b7452cc8) FORMAT -- NAME -- import PACKAGE -- Benchmark BEGIN CODE -- IO -- GLOB -- GLOB(0x55a7b73816d0) FORMAT -- NAME -- BEGIN PACKAGE -- Carp END CODE -- IO -- GLOB -- GLOB(0x55a7b73672b8) FORMAT -- NAME -- END PACKAGE -- Carp DESTROY CODE -- IO -- GLOB -- GLOB(0x55a7b7367228) FORMAT -- NAME -- DESTROY PACKAGE -- Carp begin CODE -- IO -- GLOB -- GLOB(0x55a7b73672d0) FORMAT -- NAME -- begin PACKAGE -- Carp end CODE -- IO -- GLOB -- GLOB(0x55a7b7367378) FORMAT -- NAME -- end PACKAGE -- Carp import CODE -- IO -- GLOB -- GLOB(0x55a7b7367ac8) FORMAT -- NAME -- import PACKAGE -- Carp BEGIN CODE -- IO -- GLOB -- GLOB(0x55a7b74375f0) FORMAT -- NAME -- BEGIN PACKAGE -- Time::HiRes END CODE -- IO -- GLOB -- GLOB(0x55a7b7367348) FORMAT -- NAME -- END PACKAGE -- Time::HiRes DESTROY CODE -- IO -- GLOB -- GLOB(0x55a7b7367360) FORMAT -- NAME -- DESTROY PACKAGE -- Time::HiRes begin CODE -- IO -- GLOB -- GLOB(0x55a7b7437200) FORMAT -- NAME -- begin PACKAGE -- Time::HiRes end CODE -- IO -- GLOB -- GLOB(0x55a7b75481a8) FORMAT -- NAME -- end PACKAGE -- Time::HiRes import CODE -- CODE(0x55a7b7452908) IO -- GLOB -- GLOB(0x55a7b7452da0) FORMAT -- NAME -- import PACKAGE -- Time::HiRes BEGIN CODE -- IO -- GLOB -- GLOB(0x55a7b74370b0) FORMAT -- NAME -- BEGIN PACKAGE -- Math::BigInt END CODE -- IO -- GLOB -- GLOB(0x55a7b7367708) FORMAT -- NAME -- END PACKAGE -- Math::BigInt DESTROY CODE -- IO -- GLOB -- GLOB(0x55a7b74f5588) FORMAT -- NAME -- DESTROY PACKAGE -- Math::BigInt begin CODE -- IO -- GLOB -- GLOB(0x55a7b74f5180) FORMAT -- NAME -- begin PACKAGE -- Math::BigInt end CODE -- IO -- GLOB -- GLOB(0x55a7b7408ab8) FORMAT -- NAME -- end PACKAGE -- Math::BigInt import CODE -- CODE(0x55a7b772ba98) IO -- GLOB -- GLOB(0x55a7b77503e0) FORMAT -- NAME -- import PACKAGE -- Math::BigInt
MavenやGradleの依存ライブラリ解決にかかる時間を少し短縮できるかもしれない
目的
- 未公開記事の発掘
- ニッチな話題だった
2015年からGCSにMaven Centralのコピーを公開するようになったそうです。知らなかった。
どれくらい違いが出るのか検証してみる。
検証
対象のホストは以下。
- Maven Central(repo.apache.maven.org)
- Maven Central の公式ホスト
- GCS-Asia(maven-central-asia.storage-download.googleapis.com)
- GCS の Asia 地域向けホスト
- GCS-EU(maven-central-eu.storage-download.googleapis.com)
- GCS の EU 地域向けホスト
- GCS-US(maven-central.storage-download.googleapis.com)
- GCS の US 地域向けホスト
計測は curl
で行う。それぞれのホストに対して5回。
$ for m in repo.apache.maven.org maven-central-asia.storage-download.googleapis.com maven-central-eu.storage-download.googleapis.com maven-central.storage-download.googleapis.com; do for i in {0..5}; do curl -s -k -w "${m},${i},%{time_namelookup},%{time_connect},%{time_appconnect},%{time_pretransfer},%{time_redirect},%{time_starttransfer},%{time_total}\n" -o /dev/null https://${m}/maven2/org/springframework/spring-core/5.3.8/spring-core-5.3.8.jar; done done repo.apache.maven.org,0,0.037991,0.163917,0.422735,0.422971,0.000000,0.546282,0.546399 repo.apache.maven.org,1,0.004278,0.121880,0.364024,0.364144,0.000000,0.481385,0.481420 repo.apache.maven.org,2,0.001639,0.119328,0.361785,0.361857,0.000000,0.486429,0.486477 repo.apache.maven.org,3,0.002099,0.124872,0.380066,0.380149,0.000000,0.507271,0.507390 repo.apache.maven.org,4,0.001770,0.125946,0.380944,0.381223,0.000000,0.506575,0.506630 repo.apache.maven.org,5,0.001851,0.124397,0.376576,0.376872,0.000000,0.502826,0.502898 maven-central-asia.storage-download.googleapis.com,0,0.001884,0.009613,0.055196,0.055288,0.000000,0.068453,0.224391 maven-central-asia.storage-download.googleapis.com,1,0.002481,0.013766,0.068994,0.069050,0.000000,0.088661,0.270275 maven-central-asia.storage-download.googleapis.com,2,0.003084,0.012668,0.063887,0.064081,0.000000,0.072885,0.235593 maven-central-asia.storage-download.googleapis.com,3,0.001820,0.009232,0.054758,0.054809,0.000000,0.062724,0.227797 maven-central-asia.storage-download.googleapis.com,4,0.004236,0.021772,0.079667,0.079779,0.000000,0.092243,0.263077 maven-central-asia.storage-download.googleapis.com,5,0.002611,0.044696,0.101837,0.101898,0.000000,0.118384,0.305925 maven-central-eu.storage-download.googleapis.com,0,0.001900,0.011311,0.066934,0.067014,0.000000,0.082776,0.257083 maven-central-eu.storage-download.googleapis.com,1,0.001837,0.023069,0.078694,0.078742,0.000000,0.096553,0.287659 maven-central-eu.storage-download.googleapis.com,2,0.002092,0.018734,0.327978,0.328036,0.000000,0.345902,0.521407 maven-central-eu.storage-download.googleapis.com,3,0.002118,0.019215,0.070883,0.071256,0.000000,0.083224,0.271967 maven-central-eu.storage-download.googleapis.com,4,0.004461,0.018585,0.074322,0.074431,0.000000,0.088315,0.252058 maven-central-eu.storage-download.googleapis.com,5,0.001913,0.015818,0.065436,0.065493,0.000000,0.076168,0.245924 maven-central.storage-download.googleapis.com,0,0.002025,0.016342,0.068950,0.069062,0.000000,0.082023,0.293154 maven-central.storage-download.googleapis.com,1,0.001882,0.017747,0.074114,0.074177,0.000000,0.085885,0.264096 maven-central.storage-download.googleapis.com,2,0.002242,0.016268,0.071127,0.071346,0.000000,0.086131,0.259000 maven-central.storage-download.googleapis.com,3,0.002341,0.016438,0.069612,0.069663,0.000000,0.085434,0.279309 maven-central.storage-download.googleapis.com,4,0.014452,0.028634,0.077995,0.078046,0.000000,0.088248,0.257237 maven-central.storage-download.googleapis.com,5,0.002168,0.016135,0.066451,0.066529,0.000000,0.077767,0.278174
集計結果。値の単位は秒で、5回の単純平均。最小値に(*)
を付けてみた。
Maven Central は fastly を使ってても速さが出てないようだ。キャッシュが効いてるのかどうかはわからない。 合計時間のオーダーはあまり変わらない(250msと500ms)から、データ転送は速そう。
ホスト | GCS-US | GCS-Asia | GCS-EU | Maven Central |
---|---|---|---|---|
time_namelookup | 0.004185 | 0.002686(*) | 0.002386 | 0.008271 |
time_connect | 0.018594 | 0.018624 | 0.017788(*) | 0.130056 |
time_appconnect | 0.071374 | 0.070723(*) | 0.114041 | 0.381021 |
time_pretransfer | 0.071470 | 0.070817(*) | 0.114162 | 0.381202 |
time_starttransfer | 0.084248 | 0.083891(*) | 0.128823 | 0.505128 |
time_redirect | 0 | 0 | 0 | 0 |
time_total | 0.271828 | 0.254509(*) | 0.306016 | 0.505202 |
設定の仕方
Maven
${HOME}/.m2/settings.xml
あるいは%USERPROFILE%\.m2\settings.xml
へ次のように記述する。
つまり、 settings
要素の子要素を追加すればよい。
<settings> <!-- 何かしらの設定 --> <mirrors> <mirror> <id>google-maven-central</id> <name>GCS Maven Central mirror Asia Pacific</name> <url>https://maven-central-asia.storage-download.googleapis.com/maven2/</url> <mirrorOf>central</mirrorOf> </mirror> </mirrors> <!-- 何かしらの設定 --> </settings>
Gradle
${HOME}/.gradle/init.gradle
あるいは%USERPROFILE%\.gradle\init.gradle
を作成して次のように記述する。
allprojects { buildscript { repositories { mavenLocal() maven { url "https://maven-central-asia.storage-download.googleapis.com/maven2/" } } } repositories { mavenLocal() maven { url "https://maven-central-asia.storage-download.googleapis.com/maven2/" } } }
代表値を固定する代わりに指定した範囲から確率的に取り出したサンプルを確かめる
目的
- 未公開記事の発掘
- 製品コードの特性をテストが知っているのは役割分担を間違えてるような気がする
- 値域や事後条件をアノテーションで書けるといいのにな
- jlink/jqwik の利用例を紹介します
メリットや使いどころ
パラメタライズドテスト
- テスト条件と期待値の組を複数用意して、同じテストメソッドを繰り返し呼び出す手法です
- 状態や入力値によって期待値の変化するロジックを効率的にチェックできます
- JUnit5 では拡張ライブラリの junit-jupiter-params を利用します
- JUnit4 では組み込みクラスの Parameterized テストランナーを利用します
ドメインの固有型
- たとえば、業務日付を表現するのに組み込みの
Date
クラスではなくBusinessDate
クラスを作成する設計手法です - コンパイル時や実行時にプロパティ(特性)を保護することができます
- クラスの事前条件や事後条件に記述する
- プログラミング言語レベルの設計の自由度が低下する場合があります
- 固有型そのままでは一般的なライブラリの API が利用できないとか
- 記述が冗長化する場合があります
- 組み込み型との相互変換処理を提供するとか
- システムAの業務日付とシステムBの業務日付を別々の固有型にするとか
- 共通する部分を基底クラスに導出して派生クラスにするみたいなシステム都合の対応をするとドメインから乖離してしまう
具体例
サンプルプロジェクト を作ってみました。
/todo
に REST エンドポイントを公開するだけの Spring Boot な Java アプリですController > Service > Repository
の 3 階層になっています
TodoServiceTest
はプリミティブなユニットテストです- jqwik API で書き直したプロパティベーステストが
TodoServicePropertyTest
です
- jqwik API で書き直したプロパティベーステストが
TodoServiceTest
TodoServiceTest には次のようなテストを記述しています。
- 指定した ID で取得するデータが存在する場合のテスト
- 指定した ID で取得するデータが存在しない場合のテスト
- 永続化の成功を確認するテスト
複数のパラメータを使用するので、パラメタライズドテストのための @ParameterizedTest を使っています。
しかし、@ValueSource にあまり多くの値を記述すると読みにくくなるため、代表的な値しか記述していません。
@CsvFileSource を使うと、パラメーターをクラスパスに配置した CSV ファイルから読み取るようにできます。
TodoServicePropertyTest
TodoServicePropertyTest に記述しているテストは基本的に TodoServiceTest
と同じです。
パラメタライズドテストを次のような API で置き換えています。
- プロパティベーステスト @Property
- 引数に指定した @ForAll は入力値を生成します
- 生成した入力値から所定の回数だけ、ランダムに取り出してテストを実行します
- 具体例ベーステスト @Example
- プロパティベーステストと違って 1 度しか実行しません
- API の使い方など、代表的な例を記述するのに向いていると思います
- データ駆動テスト @FromData
- @Data を指定したメソッドの返り値そのものを入力としてテストを実行します
- テストメソッドの引数は 8 個まで対応していました
ユニットテストの目的
ユニットテストの目的は、プログラムの構成部品が設計したとおりに動作することを確かめることです。
- 設計したとおりに動作する とは何?
- 設計したとおりに動作することを確かめる とは何?
- 設計 に記述された要素を実現できているか確かめること
- (あるなら)前提条件を満たしているか確かめる
- (変更しているなら)意図した状態に変更できているか確かめる
- (あるなら)入力に対する出力が正しいか確かめる
- (あるなら)事後条件を満たしているか確かめる
- 設計 に記述された要素を実現できているか確かめること
設計 に記述してあることだけを確かめるわけではないので注意が必要です。
- 設計 には 仕様 から導出された正常系だけしか記述されていない場合が多い
- 記述が不足している場合もある
- そもそも記述できない場合もある
- 主に量の都合
- 設計 に記述されてないから確認しなくていい?
- No !!!
- 設計 をインプットにテストを設計する
- テストを設計する とは何?
- プログラムの構成部品を状態遷移機械に見立てて、入力と次状態を整理すること
- 入力を構成する要素:前提条件、事前状態、入力
- 次状態を構成する要素:事後条件、事後状態、出力
ユニットテストに可能な全ての入力を与えるのは大変なので工夫します。
- 同値分割
- 値の範囲を分割して代表値を選ぶ
- 境界値分析
- 分割した範囲の境目を選ぶ
- 内、境界、外
しかし、工夫した結果の正しさを厳密に保証する手法は存在しないため、設計とは異なる観点のレビューや確認(元の要素からの過不足など)が必要になります。
プロパティベーステストの目的
- プロパティベーステストは QuickCheck という Haskell の実装から成立した手法
- ランダムな入力値生成による失敗パスの発見を目指すところがファジングと似ている
この辺の説明がシンプルで分かりやすいと思います。
後から読み解くのが難しいパラメタライズドテストやデータ駆動テストができるまで
テスト対象をステートレスにするといいことがある、というより、ステートフルだと困ることがある。 問題はテスト対象がステートフルな場合。
シナリオ
- 最初は例示ベースで状態をテストコードに埋め込んでいる
- 隠れていた事前状態が見つかったりして、だんだん例示の部分が巨大化してくる
- 例示の部分をフィクスチャーとして別の関数に分離したくなる
- フィクスチャーに分離した関数は冗長な記述が多くなりがちで、JSONやYAMLやXMLなどデシリアライズの容易な形式でテストコードの外にファイルとして置きたくなる
- この辺で、事前状態と事後状態が離ればなれになることが多い
- 後から読み解くのが難しいテストはこのように生まれている
考察
フィクスチャーが肥大化するのは、例示だけで表現しきれない組み合わせが発生しているからだと思う。 なので、フィクスチャーを外部ファイルにしたくなるタイミングで踏みとどまるべきだ。 比較的見通しのよいデシジョンテーブルを整備して、デシジョンテーブルからフィクスチャーを生成するアプローチへ切り替えた方がいい。 なお、デシジョンテーブルにすると、それぞれの因子の値域を定義したくなるので、それはそれで深入りしそうな場面でもある。
Get Started With JHipster6
目的
- 未公開記事の発掘
- JHipsterの現行バージョンはv7.9.3です Release Notes
- 少なくとも2年以上前に書いていたらしい
目的
- Get Started With JHipster6 で JHipster に入門します
- 説明書きを解釈してメモします
- 作られたソースコード
JHipster is 何
- Angular と Bootstrap と Spring Boot をいい感じに統合する Web アプリケーションフレームワークです
- 2019-05-02 に公開した JHipster 6 では Java 11 と Spring Boot 2.1.x に対応しました
- 最新バージョンは 6.10.1 (2020-07-05)
- 今は React や Quarkus や Micronaut にも対応しつつある
- 2019-05-02 に公開した JHipster 6 では Java 11 と Spring Boot 2.1.x に対応しました
- プロジェクトジェネレーター、ソースコードジェネレーターとして使います
- Angular な部分は yeoman で生成してるみたい
- Spring Boot な部分はどうしてるんだろう
- 公式ページ
- GitHub リポジトリ
JHipster のインストール
- Java 11 をインストールする
- SDKMAN! -
sdk intall java 11.0.8.hs-adpt
- Scoop -
scoop install adoptopenjdk-lts-hotspot
- SDKMAN! -
- Git をインストールする
- Scoop -
scoop install git
- Scoop -
- Node.js(LTS) をインストールする
- Scoop -
scoop install nodejs-lts
- Scoop -
- JHipster をインストールする
- NPM -
npm install -g generator-jhipster
- NPM -
プロジェクトの作成
jhipster
コマンドは今のディレクトリにソースコードを生成します。
プロジェクトのディレクトリを作成、移動してから jhipster
コマンドを実行します。対話的にいろいろと入力します。
質問 | 回答 |
---|---|
type of application | Monolithic application |
WebFlux ? | No |
base name | jhipsterblog |
package name | org.bitbucket.yujiorama.jhipsterblog |
type of authentication | JWT authentication |
type of database | SQL |
production database | PostgreSQL |
development database | H2 |
Spring cache | Ehcache |
Hibernate 2nd level cache | Yes |
building the backend | Maven |
Client framework | Angular |
Bootswatch theme | Default JHipster |
internationalization support | Yes |
native language | Japanese |
additional language | English |
testing framework | Protractor |
other generators | No |
MINGW64 ~/.go/src/bitbucket.org/yujiorama/jhipster-blog (master +) $ jhipster INFO! Congratulations, JHipster execution is complete! MINGW64 ~/.go/src/bitbucket.org/yujiorama/jhipster-blog (master +) $
もろもろ完了したらすでに git add
した状態になっているので最初のコミットを作成します。
git commit -m init
環境変数 JAVA_HOME
が Java 11 を指定しているのを確認します。
$ echo $JAVA_HOME C:/Users/user/scoop/apps/adoptopenjdk-lts-hotspot/current
Java 11 に対応するため pom.xml
を編集します。
pom.xml
@@ -21,8 +21,8 @@ <properties> <!-- Build properties --> <maven.version>3.3.9</maven.version> - <java.version>1.8</java.version> + <java.version>11</java.version> <node.version>v12.16.1</node.version> <npm.version>6.14.5</npm.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> @@ -418,8 +418,7 @@ <artifactId>maven-compiler-plugin</artifactId> <version>${maven-compiler-plugin.version}</version> <configuration> - <source>${java.version}</source> - <target>${java.version}</target> + <release>${java.version}</release> <annotationProcessorPaths> <path> <groupId>org.springframework.boot</groupId>
依存ライブラリを最新化するため pom.xml
を編集します。
./mvnw versions:update-properties
pom.xml
@@ -43,7 +43,7 @@ <jhipster-dependencies.version>3.9.0</jhipster-dependencies.version> <!-- The spring-boot version should match the one managed by https://mvnrepository.com/artifact/io.github.jhipster/jhipster-dependencies/${jhipster-dependencies.version} --> - <spring-boot.version>2.2.7.RELEASE</spring-boot.version> + <spring-boot.version>2.3.3.RELEASE</spring-boot.version> <!-- The hibernate version should match the one managed by https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-dependencies/${spring-boot.version} --> <hibernate.version>5.4.15.Final</hibernate.version> @@ -52,8 +52,8 @@ <javassist.version>3.24.0-GA</javassist.version> <!-- The liquibase version should match the one managed by https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-dependencies/${spring-boot.version} --> - <liquibase.version>3.9.0</liquibase.version> - <liquibase-hibernate5.version>3.8</liquibase-hibernate5.version> + <liquibase.version>4.0.0</liquibase.version> + <liquibase-hibernate5.version>4.0.0</liquibase-hibernate5.version> <h2.version>1.4.200</h2.version> <validation-api.version>2.0.1.Final</validation-api.version> <jaxb-runtime.version>2.3.3</jaxb-runtime.version> @@ -65,18 +65,18 @@ <maven-javadoc-plugin.version>3.2.0</maven-javadoc-plugin.version> <maven-eclipse-plugin.version>2.10</maven-eclipse-plugin.version> <maven-enforcer-plugin.version>3.0.0-M3</maven-enforcer-plugin.version> - <maven-failsafe-plugin.version>3.0.0-M4</maven-failsafe-plugin.version> + <maven-failsafe-plugin.version>3.0.0-M5</maven-failsafe-plugin.version> <maven-idea-plugin.version>2.2.1</maven-idea-plugin.version> - <maven-resources-plugin.version>3.1.0</maven-resources-plugin.version> - <maven-surefire-plugin.version>3.0.0-M4</maven-surefire-plugin.version> - <maven-war-plugin.version>3.2.3</maven-war-plugin.version> + <maven-resources-plugin.version>3.2.0</maven-resources-plugin.version> + <maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version> + <maven-war-plugin.version>3.3.1</maven-war-plugin.version> <maven-checkstyle.version>3.1.1</maven-checkstyle.version> - <checkstyle.version>8.32</checkstyle.version> - <spring-nohttp-checkstyle.version>0.0.4.RELEASE</spring-nohttp-checkstyle.version> + <checkstyle.version>8.35</checkstyle.version> + <spring-nohttp-checkstyle.version>0.0.5.RELEASE</spring-nohttp-checkstyle.version> <frontend-maven-plugin.version>1.10.0</frontend-maven-plugin.version> - <git-commit-id-plugin.version>4.0.0</git-commit-id-plugin.version> + <git-commit-id-plugin.version>4.0.2</git-commit-id-plugin.version> <jacoco-maven-plugin.version>0.8.5</jacoco-maven-plugin.version> - <jib-maven-plugin.version>2.4.0</jib-maven-plugin.version> + <jib-maven-plugin.version>2.5.2</jib-maven-plugin.version> <lifecycle-mapping.version>1.0.0</lifecycle-mapping.version> <properties-maven-plugin.version>1.0.0</properties-maven-plugin.version> <sonar-maven-plugin.version>3.7.0.1746</sonar-maven-plugin.version>
NoHttpCheck
モジュールのプロパティ名を修正するため checkstyle.xml
を編集します。
checkstyle.xml
@@ -9,7 +9,7 @@ <property name="fileExtensions" value=""/> <!-- For detailed checkstyle configuration, see https://github.com/spring-io/nohttp/tree/master/nohttp-checkstyle --> <module name="io.spring.nohttp.checkstyle.check.NoHttpCheck"> - <property name="whitelist" value="http://maven.apache.org/POM/4.0.0 + <property name="allowlist" value="http://maven.apache.org/POM/4.0.0 http://www.w3.org/2001/XMLSchema-instance http://maven.apache.org/maven-v4_0_0.xsd"/> </module> <!-- Allow suppression with comments
テストが成功するのを確認します。
./mvnw verify
アプリを起動します。
./mvnw
別のプロンプトで Protractor
による e2e テストが成功するのを確認します。
npm run e2e
ブラウザで http://localhost:8080
にアクセスしていろいろ確認します。
- ユーザー名
admin
パスワードadmin
でログインできる - 管理メニューからいろいろ見れる
- ユーザー管理ページ
http://localhost:8080/admin/user-management
- メトリクス監視ページ
http://localhost:8080/admin/metrics
- ヘルスチェック
http://localhost:8080/admin/health
- データベース
- ディスク
- アプリケーション
- 設定値一覧
http://localhost:8080/admin/configuration
- 監査ログ
http://localhost:8080/admin/audits
- ユーザー認証の成否、日時など
- ログレベル設定
http://localhost:8080/admin/logs
- Swagger Editor
http://localhost:8080/admin/docs
- H2 Database Web コンソール
http://localhost:8080/h2-console/
- ユーザー管理ページ
エンティティの生成
エンティティを追加するために必要なリソース
- データベーステーブル
- Liquibase の変更セットファイル
- JPA エンティティクラス
- Spring Data JPA のリポジトリインターフェイス
- Spring MVC の REST コントローラークラス
- Angular のリストコンポーネント、編集コンポーネント、サービスコンポーネント
- それぞれのコンポーネントのための html ページ
- すべて整っていることを確認するための結合テスト
- 十分に速く動作することを確認するための性能テスト
JHipster はこれらのリソースを自動生成します。 エンティティに関連があるときは外部キー制約なども生成します。
エンティティを自動生成する方法
- サブジェネレーター
- CLI ツール
- 対話的に必要な情報を入力します
- JDL-Studio
エンティティの生成
import-jdl
サブコマンドで JDL ファイルからエンティティを生成します。
jhipster import-jdl blog.jdl
ローカル PC でアプリを実行します。
* src/main/resources/config/application-dev.yaml
で liquibase.context
に faker
が追加されているため、ダミーデータが生成されています
* ダミーデータを生成しない場合は faker
の記述を削除します
* 一度生成してしまっている場合は H2 データベースファイルを初期化するため ./mvnw clean
を実行します
./mvnw
ブログの作成と記事の作成
ブラウザでアクセスしてデータを登録します。
admin
のブログを作成します- 作成したブログに記事を作成します
user
のブログを作成します- 作成したブログに記事を作成します
ビジネスロジックの追加
- 目的
admin
としてログインした状態でブログ一覧ページhttp://localhost:8080/blog
にアクセスすると、user
のブログも見えてしまう状態です。- ログインしているユーザーのブログだけが表示されるように、ビジネスロジックを改修します。
プロジェクトのインポートとビルドエラーの解消
プロジェクトを IntelliJ IDEA にインポートします。
次の部分がビルドエラーになるはずなので、アノテーションを追加して解消します。
IntelliJ IDEA の Spring ビルダーが解決できないだけで正常です
src/main/java/org/bitbucket/yujiorama/jhpister_blog/config/LiquibaseConfiguration.java
@@ -35,6 +35,7 @@ public class CacheConfiguration { .build()); } + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") @Bean public HibernatePropertiesCustomizer hibernatePropertiesCustomizer(javax.cache.CacheManager cacheManager) { return hibernateProperties -> hibernateProperties.put(ConfigSettings.CACHE_MANAGER, cacheManager);
src/main/java/org/bitbucket/yujiorama/jhpister_blog/config/DatabaseConfiguration.java
@@ -35,6 +35,7 @@ public class DatabaseConfiguration { * @return the H2 database TCP server. * @throws SQLException if the server failed to start. */ + @SuppressWarnings("ContextJavaBeanUnresolvedMethodsInspection") @Bean(initMethod = "start", destroyMethod = "stop") @Profile(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) public Object h2TCPServer() throws SQLException {
src/main/java/org/bitbucket/yujiorama/jhpister_blog/config/CacheConfiguration.java
@@ -29,6 +29,7 @@ public class LiquibaseConfiguration { this.env = env; } + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") @Bean public SpringLiquibase liquibase(@Qualifier("taskExecutor") Executor executor, @LiquibaseDataSource ObjectProvider<DataSource> liquibaseDataSource, LiquibaseProperties liquibaseProperties,q
ブログ一覧ページの改修
BlogResource
を改修します。
src/main/java/org/bitbucket/yujiorama/jhpister_blog/web/rest/BlogResource.java
@@ -87,8 +87,8 @@ public class BlogResource { */ @GetMapping("/blogs") public List<Blog> getAllBlogs() { - log.debug("REST request to get all Blogs"); - return blogRepository.findAll(); + log.debug("REST request to get all Blogs posted by current user"); + return blogRepository.findByUserIsCurrentUser(); } /**
アプリを ./mvnw
で実行している場合はソースコードを保存すると Spring Dev Tools の機能で自動的に再起動します。
http://localhost:8080/blog
にアクセスすると、ログインしたユーザーのブログだけが表示されるようになります。
記事一覧ページの改修
同じように EntryResource
も改修します。
src/main/java/org/bitbucket/yujiorama/jhpister_blog/web/rest/EntryResource.java
@@ -96,12 +97,7 @@ public class EntryResource { @GetMapping("/entries") public ResponseEntity<List<Entry>> getAllEntries(Pageable pageable, @RequestParam(required = false, defaultValue = "false") boolean eagerload) { log.debug("REST request to get a page of Entries"); - Page<Entry> page; - if (eagerload) { - page = entryRepository.findAllWithEagerRelationships(pageable); - } else { - page = entryRepository.findAll(pageable); - } + Page<Entry> page = entryRepository.findByBlogUserLoginOrderByDateDesc(SecurityUtils.getCurrentUserLogin().orElse(null), pageable); HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); return ResponseEntity.ok().headers(headers).body(page.getContent()); }
src/main/java/org/bitbucket/yujiorama/jhpister_blog/repository/EntryRepository.java
@@ -26,4 +26,6 @@ public interface EntryRepository extends JpaRepository<Entry, Long> { @Query("select entry from Entry entry left join fetch entry.tags where entry.id =:id") Optional<Entry> findOneWithEagerRelationships(@Param("id") Long id); + + Page<Entry> findByBlogUserLoginOrderByDateDesc(String currentUserLogin, Pageable pageable); }
アプリを ./mvnw
で実行している場合はソースコードを保存すると Spring Dev Tools の機能で自動的に再起動します。
http://localhost:8080/entry
にアクセスすると、ログインしたユーザーの投稿だけが表示されるようになります。
UI 要素の改修
- 目的
- ブログなのにコンテンツをただのテキストとして描画してる
- いい感じに描画する仕組みを追加します。
別のプロンプトで webpack
の dev
サーバーを実行すると、Browsersync がブラウザを操作して http://localhost:9000
へアクセスします。
npm start
html をエスケープしないように改修
記事一覧ページで 内容 の html をエスケープしないように改修します。Browsersync がブラウザを操作して自動的に再読み込みするはず。
src/main/webapp/app/entities/entry/entry.component.html
@@ -34,7 +34,7 @@ <tr *ngFor="let entry of entries ;trackBy: trackId"> <td><a [routerLink]="['/entry', entry.id, 'view']">{{ entry.id }}</a></td> <td>{{ entry.title }}</td> - <td>{{ entry.content }}</td> + <td [innerHTML]="entry.content"></td> <td>{{ entry.date | date:'medium' }}</td> <td> <div *ngIf="entry.blog">
記事一覧ページのレイアウトを変更
記事一覧ページがブログ風になるよう1列のスタックレイアウトに変更します。
src/main/webapp/app/entities/entry/entry.component.html
@@ -18,54 +18,31 @@ <span jhiTranslate="jhipsterblogApp.entry.home.notFound">No entries found</span> </div> - <div class="table-responsive" id="entities" *ngIf="entries && entries.length > 0"> - <table class="table table-striped" aria-describedby="page-heading"> - <thead> - <tr jhiSort [(predicate)]="predicate" [(ascending)]="ascending" [callback]="reset.bind(this)"> - <th scope="col" jhiSortBy="id"><span jhiTranslate="global.field.id">ID</span> <fa-icon icon="sort"></fa-icon></th> - <th scope="col" jhiSortBy="title"><span jhiTranslate="jhipsterblogApp.entry.title">Title</span> <fa-icon icon="sort"></fa-icon></th> - <th scope="col" jhiSortBy="content"><span jhiTranslate="jhipsterblogApp.entry.content">Content</span> <fa-icon icon="sort"></fa-icon></th> - <th scope="col" jhiSortBy="date"><span jhiTranslate="jhipsterblogApp.entry.date">Date</span> <fa-icon icon="sort"></fa-icon></th> - <th scope="col" jhiSortBy="blog.name"><span jhiTranslate="jhipsterblogApp.entry.blog">Blog</span> <fa-icon icon="sort"></fa-icon></th> - <th scope="col"></th> - </tr> - </thead> - <tbody infinite-scroll (scrolled)="loadPage(page + 1)" [infiniteScrollDisabled]="page >= links['last']" [infiniteScrollDistance]="0"> - <tr *ngFor="let entry of entries ;trackBy: trackId"> - <td><a [routerLink]="['/entry', entry.id, 'view']">{{ entry.id }}</a></td> - <td>{{ entry.title }}</td> - <td>{{ entry.content }}</td> - <td>{{ entry.date | date:'medium' }}</td> - <td> - <div *ngIf="entry.blog"> - <a [routerLink]="['/blog', entry.blog?.id, 'view']" >{{ entry.blog?.name }}</a> - </div> - </td> - <td class="text-right"> - <div class="btn-group"> - <button type="submit" - [routerLink]="['/entry', entry.id, 'view']" - class="btn btn-info btn-sm"> - <fa-icon icon="eye"></fa-icon> - <span class="d-none d-md-inline" jhiTranslate="entity.action.view">View</span> - </button> - - <button type="submit" - [routerLink]="['/entry', entry.id, 'edit']" - class="btn btn-primary btn-sm"> - <fa-icon icon="pencil-alt"></fa-icon> - <span class="d-none d-md-inline" jhiTranslate="entity.action.edit">Edit</span> - </button> - - <button type="submit" (click)="delete(entry)" - class="btn btn-danger btn-sm"> - <fa-icon icon="times"></fa-icon> - <span class="d-none d-md-inline" jhiTranslate="entity.action.delete">Delete</span> - </button> - </div> - </td> - </tr> - </tbody> - </table> + <div class="table-responsive" *ngIf="entries?.length > 0"> + <div infinite-scroll (scrolled)="loadPage(page + 1)" [infiniteScrollDisabled]="page >= links['last']" [infiniteScrollDistance]="0"> + <div *ngFor="let entry of entries; trackBy: trackId"> + <a [routerLink]="['/entry', entry.id, 'view' ]"> + <h2>{{entry.title}}</h2> + </a> + <small>Posted on {{entry.date | date: 'short'}} by {{entry.blog.user.firstName}}</small> + <div [innerHTML]="entry.content"></div> + <div class="btn-group mb-2 mt-1"> + <button type="submit" + [routerLink]="['/entry', entry.id, 'edit']" + class="btn btn-primary btn-sm"> + <fa-icon [icon]="'pencil-alt'"></fa-icon> + <span class="d-none d-md-inline" jhiTranslate="entity.action.edit">Edit</span> + </button> + <button type="submit" + [routerLink]="['/', 'entry', { outlets: { popup: entry.id + '/delete'} }]" + replaceUrl="true" + queryParamsHandling="merge" + class="btn btn-danger btn-sm"> + <fa-icon [icon]="'times'"></fa-icon> + <span class="d-none d-md-inline" jhiTranslate="entity.action.delete">Delete</span> + </button> + </div> + </div> + </div> </div> </div>
バックエンド API の安全性を高める
BlogResource
へアクセスするときは、ログインしているユーザーのリソースを扱っているかどうかをチェックするようにします。結合テストも修正しましょう。
src/main/java/org/bitbucket/yujiorama/jhpister_blog/web/rest/BlogResource.java
@@ -47,11 +49,14 @@ public class BlogResource { * @throws URISyntaxException if the Location URI syntax is incorrect. */ @PostMapping("/blogs") - public ResponseEntity<Blog> createBlog(@Valid @RequestBody Blog blog) throws URISyntaxException { + public ResponseEntity<?> createBlog(@Valid @RequestBody Blog blog) throws URISyntaxException { log.debug("REST request to save Blog : {}", blog); if (blog.getId() != null) { throw new BadRequestAlertException("A new blog cannot already have an ID", ENTITY_NAME, "idexists"); } + if (!blog.getUser().getLogin().equals(SecurityUtils.getCurrentUserLogin().orElse(""))) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("error.http.403"); + } Blog result = blogRepository.save(blog); return ResponseEntity.created(new URI("/api/blogs/" + result.getId())) .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, result.getId().toString())) @@ -68,11 +73,14 @@ public class BlogResource { * @throws URISyntaxException if the Location URI syntax is incorrect. */ @PutMapping("/blogs") - public ResponseEntity<Blog> updateBlog(@Valid @RequestBody Blog blog) throws URISyntaxException { + public ResponseEntity<?> updateBlog(@Valid @RequestBody Blog blog) throws URISyntaxException { log.debug("REST request to update Blog : {}", blog); if (blog.getId() == null) { throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); } + if (blog.getUser() != null && !blog.getUser().getLogin().equals(SecurityUtils.getCurrentUserLogin().orElse(""))) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("error.http.403"); + } Blog result = blogRepository.save(blog); return ResponseEntity.ok() .headers(HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, blog.getId().toString())) @@ -97,9 +105,14 @@ public class BlogResource { * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the blog, or with status {@code 404 (Not Found)}. */ @GetMapping("/blogs/{id}") - public ResponseEntity<Blog> getBlog(@PathVariable Long id) { + public ResponseEntity<?> getBlog(@PathVariable Long id) { log.debug("REST request to get Blog : {}", id); Optional<Blog> blog = blogRepository.findById(id); + if (blog.isPresent() && + blog.get().getUser() != null && + !blog.get().getUser().getLogin().equals(SecurityUtils.getCurrentUserLogin().orElse(""))) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("error.http.403"); + } return ResponseUtil.wrapOrNotFound(blog); } @@ -110,8 +123,14 @@ public class BlogResource { * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. */ @DeleteMapping("/blogs/{id}") - public ResponseEntity<Void> deleteBlog(@PathVariable Long id) { + public ResponseEntity<?> deleteBlog(@PathVariable Long id) { log.debug("REST request to delete Blog : {}", id); + Optional<Blog> blog = blogRepository.findById(id); + if (blog.isPresent() && + blog.get().getUser() != null && + !blog.get().getUser().getLogin().equals(SecurityUtils.getCurrentUserLogin().orElse(""))) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("error.http.403"); + } blogRepository.deleteById(id); return ResponseEntity.noContent().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, id.toString())).build(); }
src/test/java/org/bitbucket/yujiorama/jhpister_blog/web/rest/BlogResourceIT.java
@@ -37,6 +38,8 @@ public class BlogResourceIT { @Autowired private BlogRepository blogRepository; + @Autowired + private UserRepository userRepository; @Autowired private EntityManager em; @@ -52,10 +55,11 @@ public class BlogResourceIT { * This is a static method, as tests for other entities might also need it, * if they test an entity which requires the current entity. */ - public static Blog createEntity(EntityManager em) { + public Blog createEntity(EntityManager em) { Blog blog = new Blog() .name(DEFAULT_NAME) - .handle(DEFAULT_HANDLE); + .handle(DEFAULT_HANDLE) + .user(userRepository.findOneByLogin("user").get()); return blog; } /** @@ -78,6 +82,7 @@ public class BlogResourceIT { @Test @Transactional + @WithMockUser public void createBlog() throws Exception { int databaseSizeBeforeCreate = blogRepository.findAll().size(); // Create the Blog @@ -154,6 +159,7 @@ public class BlogResourceIT { @Test @Transactional + @WithMockUser public void getAllBlogs() throws Exception { // Initialize the database blogRepository.saveAndFlush(blog); @@ -166,9 +172,10 @@ public class BlogResourceIT { .andExpect(jsonPath("$.[*].name").value(hasItem(DEFAULT_NAME))) .andExpect(jsonPath("$.[*].handle").value(hasItem(DEFAULT_HANDLE))); } - + @Test @Transactional + @WithMockUser public void getBlog() throws Exception { // Initialize the database blogRepository.saveAndFlush(blog); @@ -191,6 +198,7 @@ public class BlogResourceIT { @Test @Transactional + @WithMockUser public void updateBlog() throws Exception { // Initialize the database blogRepository.saveAndFlush(blog); @@ -236,6 +244,7 @@ public class BlogResourceIT { @Test @Transactional + @WithMockUser public void deleteBlog() throws Exception { // Initialize the database blogRepository.saveAndFlush(blog);
いろいろなデプロイ
本番用のビルドでテストが成功することを確認します。
./mvnw -Pprod clean verify
データベースマイグレーション管理ツール dbmate
目的
- 未公開記事の発掘
- upとdownを同じファイルに書けるのは無駄なくてよさそう
amacneil/dbmate でデータベースマイグレーションを始めるときのガイドです。
依存関係は少ないし、凝った動作もしないのでなんかよさそう。
仕組み
- 管理対象データベースに
schema_migrations
というテーブルを作成し、マイグレーションスクリプトの適用状況を追跡します - データベースのローカルオブジェクト (テーブルやビューやシーケンスなど) を管理します
- データベースのグローバルオブジェクト (ユーザーや権限など) は管理しません
- データベーススキーマをダンプするときはデータベースソフトウェア固有のコマンドを利用します
- MySQL -
mysqldump
- PostgreSQL -
pg_dump
- MySQL -
準備
amacneil/dbmate のインストール
Go が入ってるなら go install
するだけ。
そうでなければリリースアセットから対象OSの実行可能ファイルをダウンロードします。
go install github.com/amacneil/dbmate@latest
サンプルリポジトリのダウンロード
Sakila データベースを MySQL (8.0.21) と PostgreSQL (13) のコンテナにロードする docker-compose.yml
ファイルを用意しました。
dbmate は 8.x 以上の MySQL に対応しています (5.x には対応してません)
$ git clone https://bitbucket.org/yujiorama/db-migration-base-202009 $ cd db-migration-base-202009 $ docker-compose up -d $ docker-compose ps Name Command State Ports ------------------------------------------------------------------------------------------------- mysql-demo_mysql_1 docker-entrypoint.sh mysqld Up 0.0.0.0:3306->3306/tcp, 33060/tcp mysql-demo_postgres_1 docker-entrypoint.sh postgres Up 0.0.0.0:5432->5432/tcp
後始末するときはこちらを。
# ボリュームも一緒に削除します docker-compose down -v
ガイド
1 新しい DDL/DML を用意する
new
サブコマンドを実行すると、新しい DDL/DML を記述する SQL ファイルを作成します。
引数は SQL ファイル名の一部になります。
$ dbmate --url "mysql://user:password@host:port/db" new create-todo-table Creating migration: db/migrations/yyyymmddHHMMSS_create-todo-table.sql
SQL ファイルはコメントで 2 つの部分に分かれています。
-- migrate:up -- migrate:down
migrate:up
にはup
サブコマンドで実行する SQL を記述しますmigrate:down
にはrollback
サブコマンドで実行する SQL を記述します- それぞれの部分には何行でも SQL を記述できます
ファイルを作成するディレクトリ名は --migrations-dir
オプション、あるいは、環境変数 DBMATE_MIGRATIONS_DIR
で指定できます。
環境別にディレクトリを分けるやり方はたぶん事故ります。
ブランチを別にするか、リポジトリを別にしたほうがいいでしょう。
$ dbmate --url "mysql://user:password@host:port/db" --migrations-dir mydb/migrations new create-todo-table Creating migration: mydb/migrations/20200928093003_create-todo-table.sql $ DBMATE_MIGRATIONS_DIR=otherdb/migrations dbmate --url "mysql://user:password@host:port/db" new create-todo-table Creating migration: otherdb/migrations/20200928093003_create-todo-table.sql
2 用意した DDL/DML を適用する
up
サブコマンドは SQL ファイルの migrate:up
部分を実行できます。
schema_migrations
テーブルを検索して適用済みかどうかを判断するようです。
$ dbmate --url mysql://user:pass1word@minikube:3306/sakila up
Applying: 20200928090841_create-todo-table.sql
status
サブコマンドを実行すると適用済みかどうかを確認できます。
$ dbmate --url mysql://user:pass1word@minikube:3306/sakila status [X] 20200928090841_create-todo-table.sql Applied: 1 Pending: 0
なお、複数 Pending
がある状態で up
すると順番に実行していきます。
$ dbmate --url mysql://user:pass1word@minikube:3306/sakila up Applying: 20200928093936_create-todo-table.sql Applying: 20200928094214_create-todo-table.sql $ dbmate --url mysql://user:pass1word@minikube:3306/sakila status [X] 20200928090841_create-todo-table.sql [X] 20200928093936_create-todo-table.sql [X] 20200928094214_create-todo-table.sql Applied: 3 Pending: 0
3 適用した DDL/DML をロールバックする
rollback
サブコマンドは SQL ファイルの migrate:down
部分を実行できます。
最後に適用した SQL ファイル、つまり、最後に Applied
になった SQL ファイルを対象にするようです。
$ dbmate --url mysql://user:pass1word@minikube:3306/sakila status [X] 20200928090841_create-todo-table.sql [X] 20200928093936_create-todo-table.sql [ ] 20200928094214_create-todo-table.sql Applied: 2 Pending: 1 $ dbmate --url mysql://user:pass1word@minikube:3306/sakila rollback Rolling back: 20200928093936_create-todo-table.sql $ dbmate --url mysql://user:pass1word@minikube:3306/sakila status [X] 20200928090841_create-todo-table.sql [ ] 20200928093936_create-todo-table.sql [ ] 20200928094214_create-todo-table.sql Applied: 1 Pending: 2
なお、 migrate:down
の部分に構文エラーがあると実行時エラーになります。
たぶん実行時エラーの場合もですが、ロールバックしたことにはなりません。
$ dbmate --url mysql://user:pass1word@minikube:3306/sakila status [X] 20200928090841_create-todo-table.sql [X] 20200928093936_create-todo-table.sql [X] 20200928094214_create-todo-table.sql Applied: 3 Pending: 0 $ echo '1;' >> db/migrations/20200928094214_create-todo-table.sql $ dbmate --url mysql://user:pass1word@minikube:3306/sakila rollback Rolling back: 20200928094214_create-todo-table.sql Error: Error 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '1' at line 3 $ dbmate --url mysql://user:pass1word@minikube:3306/sakila status [X] 20200928090841_create-todo-table.sql [X] 20200928093936_create-todo-table.sql [X] 20200928094214_create-todo-table.sql Applied: 3 Pending: 0
4 schema_migrations
テーブル
create
や up
や rollback
を最初に実行するとき、接続先URLで指定したデータベースへ作成します。
$ mysql --user user -p -h minikube sakila Enter password: ********* Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 33 Server version: 8.0.21 MySQL Community Server - GPL Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> desc schema_migrations; +---------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +---------+--------------+------+-----+---------+-------+ | version | varchar(255) | NO | PRI | NULL | | +---------+--------------+------+-----+---------+-------+ 1 row in set (0.00 sec) mysql> select * from schema_migrations; +----------------+ | version | +----------------+ | 20200928090841 | | 20200928093936 | | 20200928094214 | +----------------+ 3 rows in set (0.00 sec)