Flyway を導入している Spring boot にて単体テストを実施すると、「スキーマ"public"が見つかりません」とエラーが出る


概要

DB のマイグレーションツールとして、Flyway を導入したアプリケーションにて、単体テストを実行した結果、表題のエラーが発生しました。

-------------

SQL State : 90079
Error Code : 90079
Message : スキーマ "public" が見つかりません
Schema "public" not found; SQL statement:
CREATE TABLE "public"."schema_version" (
"version_rank" INT NOT NULL,
"installed_rank" INT NOT NULL,
"version" VARCHAR(50) NOT NULL,
"description" VARCHAR(200) NOT NULL,
"type" VARCHAR(20) NOT NULL,
"script" VARCHAR(1000) NOT NULL,
"checksum" INT,
"installed_by" VARCHAR(100) NOT NULL,
"installed_on" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"execution_time" INT NOT NULL,
"success" BOOLEAN NOT NULL
) [90079-196]
Line : 17


Flyway とは

こちらの記事 に書かれております通り、現在のデータベースのバージョンを確認して、未適用の DDL や SQL をアプリケーションの開始と同時に実行してくれるツールです。

このため、いかなる環境においても、アプリケーションをデプロイすると、データベースのテーブルなどのオブジェクトは、必ず同じ形になります。


DDL の配置

マイグレーションの機能を実現するため、マイグレーション用の DDL は、アプリケーションに配置します。

デフォルトの配置場所は下記です。

配置するファイルは sql です。ファイル名は、V<MajorVersion>_<MinorVersion>__<description_with_underscore>.sql の形式で配置します。

./src

|-- main
|-- java
|-- resources
|-- db
|-- migration
|-- V1_1__create_hoge_table.sql


実践例

2018 年 9 月 1 日にリリースするタイミングで、データベースに、下記のテーブルを作成します。

・company

・product

この時、src/main/resources/db/migration 配下の flyway の ddl は下記になります。


src/main/resources/db/migration

V1_1__create_company.sql

V1_2__create_product.sql

新バージョンのアプリケーションが該当環境にデプロイされて、アプリケーションが実行される時、データベースのバージョンが古い (DDL を未適用の) 場合、上記の DDL が実行されて、2 つのテーブルが作成されます。

その後、2019 年 10 月 1 日の開発において、新たにテーブルが追加されました。

・job

この時、src/main/resources/db/migration 配下の flyway の ddl に下記が追加されます。


src/main/resources/db/migration

V1_3__create_job.sql


デプロイ後、アプリケーションが実行される時、データベースのバージョンが古い場合、上記の DDL が実行されて、テーブルが作成されます。

これで、データベースのバージョンが上がり、テーブルは合計 3 つになりました。


schema_verion について

2018 年 9 月 1 日にリリースされた際の DDL が、2019 年 10 月 1 日の新規バージョンリリース時にデータベースに適用されている場合、company, product のテーブルを生成するための DDL は実行されません。

これは flyway にてバージョンを管理しているために実現されています。

バージョン管理はなにで行われているかというと、表題のエラーが発生する時に実行されている DDL で定義された schema_version というテーブルの一部です。2019 年 10 月 1 日にアプリケーションがデプロイされる前には、下記の状態になっております。

version
description
success
installed_on

1.1
create company
1
2018-09-01 07:44:56

1.2
create product
1
2018-09-01 07:44:57

2019 年 10 月 1 日にアプリケーションがデプロイされ、flyway が実行されるとき、schema_version に記録されたバージョンから、job の DDL が実行されていないことがわかります。そのため、アプリケーションがデプロイされると、job の ddl だけが実行されて、最新のバージョンになります。

version
description
success
installed_on

1.1
create company
1
2018-09-01 07:44:56

1.2
create product
1
2018-09-01 07:44:57

1.3
create job
1
2019-10-01 07:44:57


現象の原因

今回の現象は、上記の schema_version テーブルを作成する時に "public" スキーマが存在しなかったために発生しています。

なぜ、存在していなかったかというと、データベース上では、"public" スキーマは、"PUBLIC" スキーマとして全て大文字のオブジェクトとして作成されているためでした。

今回は、単体テストでアプリケーションを実行していますので、データベースは MySQL などの物理的なデータベースではなく、インメモリの H2 データベースを採用しています。

H2 データベースで schema_version テーブルを生成するための DDL は、本来スキーマを大文字にして実行されるべきのようです。


実際

CREATE TABLE "public"."schema_version" (



あるべき姿

CREATE TABLE "PUBLIC"."schema_version" (



依存性について

今回の現象は、H2 データベース、または、flyway のバージョンが古いために発生したものかと疑いましたが、そうではありませんでした。依存関係は下記のようになっています。


build.gradle

buildscript {

ext {
springBootVersion = "1.5.4.RELEASE"
}
repositories {
mavenCentral()
}
}
dependencies {
compile('org.flywaydb:flyway-core')
testCompile('com.h2database:h2')
}

また、application.properties に原因があるかとも考えましたが、そもそも flyway に関連する設定は一切入っておりませんでした。


application-test.properties

spring.datasource.username=sa

spring.datasource.password=
spring.datasource.url=jdbc:log4jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driver-class-name=net.sf.log4jdbc.DriverSpy
spring.jpa.hibernate.ddl-auto=validate
spring.h2.console.enabled=true
logging.level.jdbc=OFF
logging.level.jdbc.sqltiming=DEBUG
server.port=9000
logging.level.root=INFO
org.hibernate.SQL=enabled
spring.jpa.generate-ddl=true


対処策

こちらのページ では、flyway が実行される前に、初期化スクリプトで public スキーマを作ってしまうか、大文字の "PUBLIC" を flyway に渡すようアドバイスしています。

データベースのスキーマ名は、flyway が実行される際、flyway の Schema クラスとしてセットされます。

デバッガを設定すると、H2Schema クラス (Schema クラスの実装) に、public でスキーマ名がセットされることが確認できます。

スクリーンショット 2018-11-05 14.31.12.png

Schema クラスを使用して、schema_version テーブルが "public" スキーマに作成されると、以降のマイグレーションは、同じロジックで Schema クラスが使用され、"public" スキーマに記録されていくので、"public" スキーマが作成できればマイグレーションにてエラーは発生しないでしょう。また、単体テストでのデータベースのアクセスも public スキーマに行えば問題は発生しないはずです。

しかしながら、この方法は少々強引な方法であると考えられます。

そもそもなぜ、初期スクリプトなど用意しなければならないのでしょう。そのような面倒なことをせずとも、flyway なり H2 の JDBC なりでこのような奇妙な差異は吸収されるべきです。

そのため、flyway に大文字で "PUBLIC" という文字列を渡すことができないかという検討をはじめました。もちろん、flyway の利用者が自力で "PUBLIC" を渡す方式は考えません。限りなくツール、フレームワークにこのような処理は任せてしまいたいですからね。

そんな観点から調査した結果、ドライバーの設定に問題があることがわかりました。

単体テスト用に DriverManagerDataSource の dataSource を Bean として定義しており、この Bean 経由で単体テストではテストを実施しようと考えていました。


TestConfig(変更前).java

@Configuration

public class TestConfig {

@Autowired
Environment environment;

@Bean
public TransactionAwareDataSourceProxy dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy proxyDs = new net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy(dataSource);
dataSource.setDriverClassName("net.sf.log4jdbc.DriverSpy");
dataSource.setUrl(environment.getProperty("spring.datasource.url"));
dataSource.setUsername(environment.getProperty("spring.datasource.username"));
dataSource.setPassword(environment.getProperty("spring.datasource.password"));
return new TransactionAwareDataSourceProxy(proxyDs);
}
}


しかしながら、このドライバーの定義であると、必ず、スキーマ名が "public" になってしまうようなのです。

そしてこの差異を吸収するための最適なドライバーとして spring が提供する EmbeddedDatabaseBuilder クラスが存在しました。このクラスを使えば、H2 に最適化したドライバーを定義できます。


TestConfig(変更後).java

@Configuration

public class TestConfig {
@Bean
public TransactionAwareDataSourceProxy dataSource() {
EmbeddedDatabase ds = new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy proxyDs = new net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy(ds);
return new TransactionAwareDataSourceProxy(proxyDs);
}
}

dataSource の定義を変更することにより、スキーマ名が想定通り "PUBLIC" になりました。

スクリーンショット 2018-11-05 14.38.58.png