Spring Boot2.4で動作していたWebアプリを2.7(2.5以降)へバージョンアップしたら、アプリ起動時にdata.sqlによるDBのテーブルデータ初期化処理でエラー発生して起動できなくなったので、その時の解決方法について説明しています。
※ 本ページはプロモーションが含まれています。
Spring Bootのバージョン2.4.13で動作していたWebアプリ(Webサイト)を2.7.5にアップグレードしたら、アプリ起動時にエラーが発生して起動しない問題が発生しました(起動時のエラーログを一部抜粋)。
〜
2022-11-20 18:38:36.136 WARN 22130 --- [ restartedMain] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSourceScriptDatabaseInitializer' defined in class path resource [org/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.class]: Invocation of init method failed; nested exception is org.springframework.jdbc.datasource.init.ScriptStatementFailedException: Failed to execute SQL script statement #1 of URL [file:/Applications/Eclipse_2020-06.app/Contents/workspace/test-h2-2/target/classes/data.sql]: INSERT INTO diary(comment, create_datetime) VALUES('今日は晴れ。コメント1', LOCALTIME()); nested exception is org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "DIARY" not found (this database is empty); SQL statement:
INSERT INTO diary(comment, create_datetime) VALUES('今日は晴れ。コメント1', LOCALTIME()) [42104-214]
〜
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-11-20 18:38:36.188 ERROR 22130 --- [ restartedMain] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSourceScriptDatabaseInitializer' defined in class path resource [org/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.class]: Invocation of init method failed; nested exception is org.springframework.jdbc.datasource.init.ScriptStatementFailedException: Failed to execute SQL script statement #1 of URL [file:/Applications/Eclipse_2020-06.app/Contents/workspace/test-h2-2/target/classes/data.sql]: INSERT INTO diary(comment, create_datetime) VALUES('今日は晴れ。コメント1', LOCALTIME()); nested exception is org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "DIARY" not found (this database is empty); SQL statement:
INSERT INTO diary(comment, create_datetime) VALUES('今日は晴れ。コメント1', LOCALTIME()) [42104-214]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1804) ~[spring-beans-5.3.23.jar:5.3.23]
〜
このエラーは、起動時のDBデータ初期化処理でdata.sqlファイルにある"INSERT INTO diary ~"というSQLを実行しようとしたけど、diaryというテーブルが存在しないぞというエラー内容です。
Spring Boot2.4で正常に動作していた時は、Diaryというエンティティクラスがあり、Spring Data JPAの機能でそのDiaryクラスが自動でDiaryテーブルを生成して、DMLのdata.sqlファイルにdiaryテーブルの初期化データを生成するinsert SQLがあり、そのinsert SQLが起動時に問題なく実行されてデータ初期化ができていました。
これがSpring Bootのバージョンを2.7へアップしたら、アプリ起動時にエラーが発生したという問題です。本記事では、このエラーを解決するためにした事を書いています。
今回のエラーとは関係ないかもしれませんが、Spring BootアプリのDBはインメモリのH2を使用しています。
どうやらSpring Bootのバージョン2.4から2.5へのバージョンアップ時に、data.sqlやschema.sqlファイルによるSQLスクリプトのデータ初期化処理周りのサポートが再設計されたようです。Spring Boot2.5のリリースノートから一部抜粋します。
Hibernate and data.sql
By default, data.sql scripts are now run before Hibernate is initialized. This aligns the behavior of basic script-based initialization with that of Flyway and Liquibase. If you want to use data.sql to populate a schema created by Hibernate, set spring.jpa.defer-datasource-initialization to true.
(引用元:Spring Boot 2.5 Release Notes)
抜粋部分をDeepLで翻訳してみます。
「Hibernateとdata.sql
デフォルトでは、data.sql スクリプトは、Hibernate が初期化される前に実行されるようになりました。これは、基本的なスクリプトベースの初期化の振る舞いを Flyway と Liquibase のそれと一致させるものです。Hibernate が作成したスキーマに data.sql を使用したい場合は、spring.jpa.defer-datasource-initialization を true に設定します。」
バージョン2.4までは、Hibernateの初期化がされた後にdata.sqlファイルに書かれているSQLスクリプトが実行されていたが、2.5からはその順序が逆になったようです。(ちなみにHibernateの初期化というのは、Javaのエンティティクラスからテーブルが自動生成される機能です。)
ですので、今回のエラーはSpring Bootをバージョンアップした事により、Diaryエンティティクラスがdiaryテーブルを生成する前に、data.sqlのInsert SQL("INSERT INTO diary ~")が実行されてdiaryテーブルが無いよというエラーが発生するようになってしまいました。
そして、Hibernateが作成したスキーマにdata.sqlを使用したい場合、つまり、バージョン2.5以上でもHibernateの初期化が完了した後にdata.sql(schema.sqlも)を実行するには、spring.jpa.defer-datasource-initializationをtrueに設定すればいいという説明があります。
という事で、このエラーを解決するにはapplication.propertiesファイルにこの1行を追加します。
spring.jpa.defer-datasource-initialization=true
私の場合もこの1行を追加したら、Spring Bootのバージョンを2.7.5(2.5以降)にアップしても正常に動作するようになりました!
上記ではspring.jpa.defer-datasource-initializationを指定する事で問題解決しましたが、別の方法として、SQLファイルでデータ初期化(data.sql、DML)だけでなく、DBスキーマ(DDL)も設定するようにすれば解決できます。
明示的にDDLとDMLのSQLファイルを指定するには、application.propertiesファイルで「spring.sql.init.*」を指定します。
spring.sql.init.mode=always
spring.sql.init.schema-locations=classpath:schema.sql
spring.sql.init.data-locations=classpath:data.sql
spring.sql.init.schema-locationsでschema.sql、spring.sql.init.data-locationsでdata.sqlを指定したので、src/main/resourcesディレクトリ(Spring Bootのクラスパスが通っているディレクトリ)にschema.sqlファイルを新規作成してDDL(create table diary ~)を書きます。
これで、Spring Bootのアプリ起動時にschema.sqlとdata.sqlのSQLが実行されて、diaryテーブルが生成されてデータも初期化する事ができるようになります。
ただ、それとは別にSpring Data JPA機能(Hibernateの初期化)でDiaryエンティティクラスからdiaryテーブルを自動生成してしまうと、SQLでテーブルとデータを初期化した後にHibernateの初期化も実行されてSQLで初期化したデータが消えてしまうので、HibernateのDBテーブル初期化を実行されないようにする設定をします。
HibernateのDBテーブル初期化を実行されないようにするには、application.propertiesファイルにこの1行を追加します。
spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.ddl-autoをnoneに指定する事で、アプリ起動時にSpring Data JPAによって内部実装のHibernateがテーブル作成機能が実行されなくなります。この方法ならspring.jpa.defer-datasource-initializationプロパティを指定する必要は無いです。