8
11

More than 5 years have passed since last update.

Java 8 の Spring Boot + QueryDSL アプリケーションの Java 11 へアップグレード

Last updated at Posted at 2019-01-12

概要

Java 8 で動いているアプリケーションを Java 11 へ移行するにあたり、実現方法を調査しました。
Spring Boot が JOOQ を正式にサポートしており、QueryDSL の開発も止まっているように見えるので、今後より利用される割合は減っていく可能性は高いですが
多めの変更が必要となったので備忘録として変更内容を残しておきます。

フレームワークなど

開発環境は IntelliJ IDEA を想定しています。
アプリケーションは、下記の技術スタックで構成されています。

  • Jdk 1.8.0.131
  • Gradle 3.5-rc-2
  • Spring Boot 1.5.4.RELEASE
  • QueryDSL 4.2.1

プロジェクトの構成

タスクランナーおよび依存性の解決は、Gradle を利用しています。
build.gradle の内容を記載します。

build.gradle
buildscript {
  ext {
    springBootVersion = "1.5.4.RELEASE"
  }
  repositories {
    mavenCentral()
  }
  dependencies {
      classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

apply plugin: "java"
apply plugin: "org.springframework.boot"

version = "1.0.0"
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}
dependencies {

    compile('com.querydsl:querydsl-apt:4.2.1')
    compile('com.querydsl:querydsl-sql:4.2.1')
    compile('com.querydsl:querydsl-sql-spring:4.2.1')
    compile('com.querydsl:querydsl-jpa:4.2.1')

    compile("org.springframework.boot:spring-boot-starter-jdbc")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile('org.springframework.boot:spring-boot-starter')
    compile('org.springframework.boot:spring-boot-starter-aop')
    compile("org.springframework.boot:spring-boot-starter-web")

    compile "org.lazyluke:log4jdbc-remix:0.2.7"
    compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")
    compile('org.springframework.boot:spring-boot-starter-actuator')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    runtime('mysql:mysql-connector-java')
    compile('org.projectlombok:lombok')
    testRuntime('com.h2database:h2')
    testCompile('org.dbunit:dbunit:2.5.1')
    testCompile('org.springframework.boot:spring-boot-starter-test')

}

sourceSets {
    generated
}
sourceSets.generated.java.srcDirs = ['build/classes/main']
configurations {
    querydslapt
}

/**** QueryDSL Class Generate Script to avoid lombok error ****/
def queryDslOutput = file("src/main/generated")
sourceSets {
    main {
        java {
            srcDir queryDslOutput
        }
    }
}

task generateQueryDSL(type: JavaCompile, group: 'build') {
    source = sourceSets.main.java
    classpath = configurations.compile
    destinationDir = queryDslOutput
    options.compilerArgs = [
            "-proc:only",
            "-processor", 'com.querydsl.apt.jpa.JPAAnnotationProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor'
    ]
}
compileJava.dependsOn(generateQueryDSL)

clean {
    delete queryDslOutput
}

QueryDSL のタスク

上記の build.gradle には、generateQueryDSL というタスクを定義しています。このタスクは、JavaCompile として定義しています。タスクが実行されると、QueryDSL のソースコード (以下 Q java ファイルと呼びます) を生成します。QueryDSL の詳細は省略しますが、このフレームワークは Annotation Processor Tool (APT) の機能を使用しています。APT にて Java のクラスを抽出し、抽出したクラスに対するクエリをメソッドチェーンで実装するためのクラスファイルを生成します。

JPAAnnoatationProcessor

build.gradle の generateQueryDSL にはいくつかのプロパティがありますが、JPAAnnotationProcessor は、 options.compilerArgs プロパティにて APT として定義しています。この APT は Java をコンパイルする際、下記のアノテーションがついているクラスをコンパイルのターゲットとします。

@Entity
@MappedSuperclass
@Embeddable
@Embedded

なお、generateQueryDSL タスクのプロパティは下記の意味合いとなります。

  • source
    • main 配下のソースコードを対象に
  • destination
    • build.gradle で compile が設定されているライブラリをクラスパスに通す
  • destinationDir
    • APT の成果物を配置するディレクトリ。src/main/generated になります。SourceSets の定義により、src/main/generated がソースコードの配置場所として IDE に認識されます。
  • options.compilerArgs
    • APT のクラスを指定。QueryDSL の APT が動作する際に必要となるので、Lobok の APT も使用している。APT はカンマで区切りで指定。

参考
Class JavaCompile
https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/compile/JavaCompile.html

generateQueryDSL の実行

上記の定義に従い、generateQueryDSL タスクは実行され、最終的に Q java ファイルを生成します。
例えば、下記のエンティティの場合、

MemberInformation.java
@Entity
@Getter
@NoArgsConstructor
public class MemberInformation implements Serializable {
    @Id
    private Integer memberId;

    private Integer artistId;
}

下記の Q java ファイルが成果物として生成されます。

QMemberInformation.java
@Generated("com.querydsl.codegen.EntitySerializer")
public class QMemberInformation extends EntityPathBase<MemberInformation> {

    private static final long serialVersionUID = -575571004L;

    public static ConstructorExpression<MemberInformation> create(Expression<Integer> artistId) {
        return Projections.constructor(MemberInformation.class, new Class<?>[]{int.class, int.class}, artistId, memberId);
    }

    private static final PathInits INITS = PathInits.DIRECT2;

    public static final QMemberInformation memberInformation = new QMemberInformation("memberInformation");

    public final NumberPath<Integer> artistId = createNumber("artistId", Integer.class);

    public final NumberPath<Integer> memberId = createNumber("memberId", Integer.class);
   // 以下省略
}

Q java ファイルが生成されるディレクトリは、下記の構成の場合、src/main/generated 配下になります。
src/main/generated 配下のソースコードはコンパイル対象のソースコードとして、Gradle に設定されているので src/main/java 配下のクラスはこれらのソースコードを参照することができます。

<Project Root>
  |- src
  |   |- main
  |   |    |- generated
  |   |    |     |- QMemberInformation.java
  |   |    | - java  --> 配下の Java ファイルはQMemberInformation.java を参照可能
  |   |    |     |- MemberInformation.java
  |   |- test

Java 11 への移行

Java 11 へ移行するにあたり、Spring Boot と Gradle のバージョンをあげました。
IntelliJ IDEA では、Command + ; -> [Project] から JDK 11 をセットしました。
また、jenv を利用して、java 11 をデフォルトの JDK としてセットしました。

Spring Boot

公式ページ によると、Java 9 以降のサポートは、1.5.x では予定していないそうです。
このため、Spring Boot 2.1.1 にアップグレードします。

Spring Boot 2 is the first version to support Java 9 (Java 8 is also supported). If you are using 1.5 and wish to use Java 9 you should upgrade to 2.0 as we have no plans to support Java 9 on Spring Boot 1.5.x.

Gradle

Gradle 3.5 では Java 11 が動作しませんでした。また、Gradle 4.9 を利用すると、最小限の build.gradle の修正で正常に動作しましたが、テストクラスに内部クラスがあると正常に動作しないというバグがありました。

org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':test'.
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:110)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:77)

 ......... omitted ..........

Caused by: java.lang.IllegalArgumentException: Unsupported class file major version 55
        at org.objectweb.asm.ClassReader.<init>(ClassReader.java:166)
        at org.objectweb.asm.ClassReader.<init>(ClassReader.java:148)
        at org.objectweb.asm.ClassReader.<init>(ClassReader.java:136)
        at org.gradle.api.internal.tasks.testing.detection.AbstractTestFrameworkDetector.classVisitor(AbstractTestFrameworkDetector.java:123)
        ... 67 more

このため、Gradle のバージョンを 5.0 にあげました。
gradle のバージョンをあげるためには、<PROJECT_ROOT>/gradle/wrapper/gradle-wrapper.properties の distributionUrl を変更します。

gradle-wrapper.properties(変更前)
distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-rc-2-all.zip
gradle-wrapper.properties(変更後)
distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-all.zip

参考
Building with JDK 11 breaks if Tests have inner classes
https://github.com/gradle/gradle/issues/7723

build.gradle の変更

Gradle が 3.5 -> 5.0 に至る過程において、build.gradle の仕様が大きく変わった変わったので、変更部分を書き直しました。
また、Gradle の動作の変更によるものかどうかはわかりませんが、IntelliJ IDEA の動作が変わりましたので、この動作変更を吸収する設定を build.gradle に書きました。

java 11 を利用した Spring Boot + QueryDSL アプリケーションにおいて、build.gradle は最終的にこのような形になりました。

build.gradle
buildscript {
    ext {
        // ① Spring Boot のバージョン
        springBootVersion = '2.1.1.RELEASE'
    }
    repositories {
        mavenCentral()
        // ② リポジトリの追加
        maven { url 'http://repo.spring.io/plugins-release' }
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
    dependencies {
        classpath "net.ltgt.gradle:gradle-apt-plugin:0.19"
    }
}

apply plugin: "java"
apply plugin: "org.springframework.boot"
//③ プラグイン追加
apply plugin: 'io.spring.dependency-management'
apply plugin: 'net.ltgt.apt'

version = "1.0.0"
//④ Java のバージョン
sourceCompatibility = 11

repositories {
    mavenCentral()
}

dependencies {

    compile('com.querydsl:querydsl-apt:4.2.1')
    compile('com.querydsl:querydsl-sql:4.2.1')
    compile('com.querydsl:querydsl-sql-spring:4.2.1')
    compile('com.querydsl:querydsl-jpa:4.2.1')

    compile("org.springframework.boot:spring-boot-starter-jdbc")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile('org.springframework.boot:spring-boot-starter')
    compile('org.springframework.boot:spring-boot-starter-aop')
    compile("org.springframework.boot:spring-boot-starter-web")

    compile "org.lazyluke:log4jdbc-remix:0.2.7"
    compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")
    compile('org.springframework.boot:spring-boot-starter-actuator')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    runtime('mysql:mysql-connector-java')
    compile('org.projectlombok:lombok')
    testRuntime('com.h2database:h2')
    testCompile('org.dbunit:dbunit:2.5.1')
    testCompile('org.springframework.boot:spring-boot-starter-test')

}

// ⑤ Q Java ファイルをソースコードとする
def queryDslDir = "./out/production/classes/generated"
def queryDslOutput = file("${queryDslDir}")
sourceSets {
    main {
        java {
            srcDir queryDslDir
        }
    }
}

// ⑥ APT の依存性
def queryDSLVersion = "4.2.1"
dependencies {
    compile "com.querydsl:querydsl-jpa:${queryDSLVersion}"
    annotationProcessor(
            "com.querydsl:querydsl-apt:${queryDSLVersion}:jpa"
            , "javax.annotation:javax.annotation-api:1.3.2"
            , "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final"
    )
}

dependencies {
    compile "org.projectlombok:lombok:1.18.4"
    annotationProcessor "org.projectlombok:lombok:1.18.4"
}

dependencies {
    testCompile "org.projectlombok:lombok:1.18.4"
    testAnnotationProcessor(
            "org.projectlombok:lombok:1.18.4"
    )
}

// ⑦ Gradle 実行時の挙動
compileJava.doFirst {
    delete file("${queryDslDir}/com")
    ant.echo(message: "compileJava... we successully deleted files")
}

def gradleGeneratedDir = "./build/generated/source/apt"
def gradleQueryDslOutput = file("${gradleGeneratedDir}")
task cleanUpQueryDsl(type: Copy) {
    from "${gradleGeneratedDir}/main"
    into "${queryDslDir}"
    doLast {
        delete gradleQueryDslOutput
    }
}
compileJava.finalizedBy(cleanUpQueryDsl)

// ⑧ clean 時の挙動
clean {
    delete gradleQueryDslOutput
    delete queryDslOutput
}

以下に変更内容を説明します。

① Spring Boot のバージョン

Spring Boot のバージョンを 2.1.1 にアップグレードします。

② リポジトリの追加

Spring Boot および Gradle のプラグインのリポジトリを追加します。

③ プラグイン追加

Spring Boot 2.X 系を採用すると、どうやらBOM プロジェクト が加わったのか、depedency-management が必要となりました。
また、net.ltgt.apt を追加します。このプラグインが設定されると、 Gradle にて APT によるソースコード (この場合 Q java ファイル) が生成される際、以下のフォルダにソースコードを生成されるようになります。

/build/generated/source/apt

<Project Root>
  |- build
  |   |- classes
  |   |     |- java
  |   |     |    |- MembershipInfo.class
  |   |     |    |- QMembershipInfo.class
  |   |- generaeted
  |   |     |- source
  |   |     |   |- apt
  |   |     |   |    |- QMembershipInfo.java

このプラグインがないと、Gradle 経由で APT にてソースコードが生成される際、.class ファイルと同じディレクトリに、Q java ファイルが生成されます。

/build/classes/java

<Project Root>
   |- build
   |    |- classes
   |    |     |- java
   |    |     |   |- MembershipInfo.class
   |    |     |   |- QMembershipInfo.class
   |    |     |   |- QMembershipInfo.java

アプリケーションに APT から生成されたソースコードを混ぜる必要はないので、このプラグインを使用します。

参考
io.spring.dependency-management
https://plugins.gradle.org/plugin/io.spring.dependency-management

net.ltgt.apt
https://plugins.gradle.org/plugin/net.ltgt.apt

④ Java のバージョン

Java のバージョンを 11 にアップグレードします。

⑤ Q Java ファイルをソースコードとする

ここが最大の山場になりました。Java 1.8 と Java 11 で IntelliJ IDEA に下記の差異が生じました。

  • Java1.8
    IntelliJ でビルドしても Q Java ファイルを生成しない。

  • Java 11
    IntelliJ でビルドすると、Q Java ファイルを生成する。ソースコードが生成されるディレクトリは、out/production/classes/generated ディレクトリ配下になる。

この動作を変える方法があるかもしれませんが、わかりませんので IntelliJ の規定の動作に合わせることに。これで、IntelliJ でビルドするたびにout 配下の generated ディレクトリに生成される Q java ファイルがソースコードとして認識されるようになりました。

<Project Root>
  |-out
  |  |-production
  |  |    |- classes
  |  |    |     |- MembershipInfo.class
  |  |    |     |- QMembershipInfo.class
  |  |    |- genereated
  |  |    |     |- QMembershipInfo.java

⑥ APT の依存性

GitHubのイシュー によると、Gradle 5.0 からクラスパスから自動的にアノテーションプロセッサーを取得しなくなるとのこと。
イシューのページにて記述されているように、明示的に dependencies に AnnotationProcessor を追加します。lombok はテストクラスでも使用しているので、testAnnotationProcessor を追加します。

また 公式ページ にて、annotation processor ごとに、Annotation Processor を別々に定義するよう指示があるので、queryDSL と lombok の dependencies は、通常の dependencies とは別々に定義します。

Since implementation details matter for annotation processors, they must be declared separately on the annotation processor path. Gradle ignores annotation processors on the compile classpath.

なお、queryDSL の dependencies の AnnotationProcessor に、下記 3 つの引数を指定しています。

  1. com.querydsl:querydsl-apt:${queryDSLVersion}:jpa
  2. javax.annotation:javax.annotation-api:1.3.2
  3. org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final

1 については、 QueryDSL の APT となり、バージョンの後に "jpa" を設定している jar の参照が必要になります。2, 3 については、1 のみの記述だと、IntelliJ からのリビルド、テストは成功するものの、gralew 経由で compileJava タスクにてコンパイルする際に、Q java ファイルの生成に失敗するためか、シンボルが見つからない、というエラーが発生します。

//src/main/java/jpaandquerydsl/adapter/EntityDSL.java:8: error: cannot find symbol
import QMemberInformation;

2, 3 のライブラリを引数に設定すると gradlew 経由で正常に compileJava が終了します。

参考
Gradle 5.0 に備えて annotationProcessor について調べる
https://qiita.com/opengl-8080/items/08a9cbe973fad53d93a7

Using QueryDSL annotation processor with Gradle and IntelliJ IDEA
https://blog.jdriven.com/2018/10/using-querydsl-annotation-processor-with-gradle-and-intellij-idea/

⑦ Gradle 実行時の挙動

上述のように、Gradle にて compileJava タスクを実行すると、build 配下に Q java ファイルを生成します。build 配下に Q java ファイルが生成される際、多くの場合、out 配下の generated ディレクトリに、IntelliJ によって生成された Q java ファイルが生成されています。この状態で、Gradle にて compileJava タスクを走らせると、out 配下の generated ディレクトリに Q java ファイルが存在するため、下記のようなエラーが発生して、タスクが失敗します。

error: Attempt to recreate a file for type QMemberShipInfo.java

すでに存在しているソースコードの配置場所に同じソースコードを生成しようとして失敗しました、という内容だと思います。
これを避けるため、compileJava が実行される前に、必ず、out/production/classes/generated 配下のディレクトリを全て削除します。out/production/classes/generated 配下の Q java ファイルが削除されたので、Gradle でも Q java ファイルを生成できるようになります。
一方、compileJava 中には、Q java ファイルが build 配下に配置されている必要がありますが、compileJava タスク終了後には、Q java ファイルの class ファイルが build 配下の classes フォルダ配下に生成されるので、build/generated 配下の Q java ファイルの役目は終了します。

代わりに、IntelliJ が参照する Q java ファイルが、out/production/classes/generated フォルダ配下に配置されている必要が生じます。
つまり、compileJava 終了後には、build/generated フォルダ配下の Q java ファイルは不要となり、out/production/classes/generated フォルダ配下に Q java ファイルが必要となります。
このため、compileJava が終了すると必ず、cleanUpQueryDsl タスクを実行させています。

compileJava タスクの流れは下記になります。

  1. compileJava 開始前、out/production/classes/generated フォルダ配下の Q java ファイルを削除。再帰的にフォルダを削除するため、file オブジェクトを引数に指定する。
  2. compileJava が実行されると、ltgt プラグインのおかげで Q java ファイルが build/generated 配下に生成される。
  3. compileJava 終了後、build/generated フォルダ配下の Q java ファイルを out/production/classes/generated 配下にコピーする。
  4. Q java ファイルのコピー終了後、役目を終えた build/generated 配下の Q java ファイルをすべて削除する。

この動作になるので、java 1.8 の時とは異なり、src/generated 配下に Q java ファイルを生成する必要が無くなりました。
そのため、src/generated フォルダ配下に Q java ファイルを生成していたタスクである、generateQueryDSL タスクは削除します。

⑧ clean 時の挙動

必ずしも必要でありませんが、clean を実行したら Q java ファイルも削除します。
java 1.8 までは、src/generated 配下の Q java ファイルを削除していましたが、java 11 にアップグレードした際、このフォルダは生成されなくなりました。
build、および、out 配下のフォルダを削除します。

その他変更点

その他、Java や Spring に関して、変更をする必要が生じた点を列挙しておきます。

TestComponent の廃止

Spring Boot を 2.x にアップグレードした結果、@TestComponent をつけているクラスが Bean として生成されなくなり、インジェクションに失敗するようになりました。そのため、@TestComponent@Component に変更しました。

Beanのオーバーライドを許可

プロダクションコードにて dataSource を定義している一方、別途テストクラスにも dataSource メソッドを定義していました。

  • プロダクション
Config.java
    @Bean
    public DataSource dataSource() {

        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(environment.getProperty("spring.datasource.driver-class-name"));
        dataSource.setUrl(environment.getProperty("spring.datasource.url"));
        dataSource.setUsername(environment.getProperty("spring.datasource.username"));
        dataSource.setPassword(environment.getProperty("spring.datasource.password"));
        return new TransactionAwareDataSourceProxy(dataSource);
    }
  • テスト
TestConfig.java
    @Bean
    public TransactionAwareDataSourceProxy dataSource() {
        net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy proxyDs = null;
        if ("jdbc:log4jdbc:h2:mem:testDB;MODE=MySQL;DB_CLOSE_ON_EXIT=FALSE".equals(environment.getProperty("spring.datasource.url"))) {
            EmbeddedDatabase ds = new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
            proxyDs = new net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy(ds);
            return new TransactionAwareDataSourceProxy(proxyDs);
        }
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        proxyDs = new net.sf.log4jdbc.sql.jdbcapi.DataSourceSpy(dataSource);
        dataSource.setDriverClassName(environment.getProperty("spring.datasource.driver-class-name"));
        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);
    }

この状態でアプリケーションを実行した結果、BeanDefinitionOverrideException が発生しました。

Caused by: org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'dataSource' defined in com.kasakaid.jpaandquerydsl.spring.TestConfig: Cannot register bean definition [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=testConfig; factoryMethodName=dataSource; initMethodName=null; destroyMethodName=(inferred); defined in com.kasakaid.jpaandquerydsl.spring.TestConfig] for bean 'dataSource': There is already [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=config; factoryMethodName=dataSource; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [com/kasakaid/jpaandquerydsl/spring/Config.class]] bound.
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.registerBeanDefinition(DefaultListableBeanFactory.java:894)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(ConfigurationClassBeanDefinitionReader.java:274)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:141)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:117)
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:327)
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:232)
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:275)
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:95)
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:691)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:528)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:127)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:117)
    ... 24 more

GitHubのイシュー によると、Spring Boot では、デフォルトで bean のオーバーライドを許可していない、とのことです。
プロダクション用、テスト用のが参照する application.properties に、設定を追加して問題を解消しました。

application.properties
spring.main.allow-bean-definition-overriding=true

Spring Boot 2.1.x disables the overrides of this type of bean by default. Try adding the following line to the application properties:
spring.main.allow-bean-definition-overriding=true
This re-enables the previous behavior.

GeneratedValue の値を変更

JPA を使用していると、ID の生成方法を@GeneratedValueにて指定する必要があります。SpringConnectionProvider を利用して JPA と QueryDSL を共存させている場合、デフォルト値である GenerationType.AUTO を指定すると、hibernate_sequence が存在しない、というエラーになるようです。

Caused by: org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing sequence [hibernate_sequence]
    at org.hibernate.tool.schema.internal.AbstractSchemaValidator.validateSequence(AbstractSchemaValidator.java:184)
    at org.hibernate.tool.schema.internal.AbstractSchemaValidator.performValidation(AbstractSchemaValidator.java:100)
    at org.hibernate.tool.schema.internal.AbstractSchemaValidator.doValidation(AbstractSchemaValidator.java:68)
    at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.performDatabaseAction(SchemaManagementToolCoordinator.java:191)
    at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.process(SchemaManagementToolCoordinator.java:72)
    at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:310)
    at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:467)
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:939)
    at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:57)
    at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:365)
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:390)
    ... 43 more

GenerationType を明示すると、エラーが解消します。
下記の場合は、IDENTITY にてエラーを回避しています。

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

findOne メソッドの代替

JpaRepository から findOne メソッドが廃止されました。Optional findById(S) で代替する必要があります。

hibernate.dialect' not set の解消

下記のエラーが発生するようになったので、利用している DB を明示的に、application-.properties に追記しました。この場合は、application-development.properties に追記しています。

application-development.properties
spring.jpa.database=mysql

出力したエラー。

Caused by: org.hibernate.HibernateException: Access to DialectResolutionInfo cannot be null when 'hibernate.dialect' not set
    at org.hibernate.engine.jdbc.dialect.internal.DialectFactoryImpl.determineDialect(DialectFactoryImpl.java:100)
    .... omitted .......
    at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:94)
    at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:263)
    ... 28 more

補足

QueryDSL を使用するメリットとしては、下記のようなことがあると思います。

文字列でクエリを組み立てる必要がない

メソッドチェーンにして可読性の高い Java のソースコードとして、クエリを組み立てることができます。下記はリポジトリに素朴な形で記述していますが、クエリ用のクラスとして、QueryDSL 用のクラスを再利用可能なコンポーネントを作成することも可能になります。
実行時に生成されるクエリと QueryDSL のメソッドはほぼ等価になるので、HQL を使うことによって発生する意味不明なエラーに悩まされません。トラブルシュートが容易になります。

MemberInformationRepository.java
public class MemberInformationRepository {
    private QMemberInformation member = QMemberInformation.memberInformation;
    private QMemberInformation m = new QMemberInformation("m");
    public List<MemberInformation> find() {
        return sqlQueryFactory.select(
                Projections.constructor(MemberInformation.class,
                        m.artistId.as(m.artistId),
                        m.memberId
                )
        )
                .from(member, m)
                .fetch()
                ;
    }
}

引数なしのコンストラクタを定義する必要がない

Hibernate を使用すると、エンティティに引数なしのコンストラクタを定義しないといけません。引数なしのコンストラクタは、エンティティを本来あるべき状態でなくとも生成できてしまいます。例えば、MemberInformation エンティティには、必ず artistId と memberId をセットしている必要がありますが、引数なしのコンストラクタを定義するとこの前提が崩れてしまいます。ソースコードから引数なしのコンストラクタが使われてはならないことが読み取れません。

// 正しい利用方法
MemberInformation valid = new MemberInformation(1, 1);
// 誤った利用方法
MemberInformation inValid = new MemberInformation();

誤った利用方法を許容してしまう段階で、Hibernate には欠陥があり、この欠陥により高い危険性が生じると考えています。データ取得については、 Hibernate に依存しなくなる (HQL も利用しない) ことで、引数なしのコンストラクタを定義する必要がなくなります。エンティティを安全に使うことができます。
ただし、QueryDSL を利用していても危険性が伴う場合があります。エンティティをデータスラスにしてしまい、すべてのフィールドに Setter を用意する方法でデータをエンティティにセットできてしまうのです。

Setter にてデータをセット

List dtos = query.select(
Projections.bean(UserDTO.class, user.firstName, user.lastName)).fetch();

私は、Setter については実装する必要がないと考えています。というのも、Setter でデータをセットできてしまうと、インスタンス生成後に外部からいつでもどこでもデータをセットできてしまいます。仮に振る舞いを持たない DTO にデータをセットする場合でも思わぬ副作用が出る場合があります。イミュータブルなオブジェクトになるよう、Projections.bean の方法は利用せず、Projections.constructor にてデータをセットする方がよいと考えています。

コンストラクタにてデータをセット

List dtos = query.select(
Projections.constructor(UserDTO.class, user.firstName, user.lastName)).fetch();

なお、フィールドに直接データをセットする方法もありますが、コンストラクタを定義して、暗黙的にフィールドに値をセットするのであれば、コンストラクタからデータをセットした方がわかりやすいのではないかと思います。
引数なしのコンストラクタを定義して、フィールドからデータをセットすると非常に使い勝手の悪いオブジェクトになるので、この方法はNGです。

List dtos = query.select(
Projections.fields(UserDTO.class, user.firstName, user.lastName)).fetch();

参考
3.2. Result handling
http://www.querydsl.com/static/querydsl/latest/reference/html/ch03s02.html

アノテーションを削減できる

Hibernate を使用すると @Column@ManyToOneなど、ドメイン (業務知識) に関係のないアノテーションを記述しないといけません。Hibernate の利用をやめるとこのような決め事は一切なくなります。エンティティにはエンティティの@Entityのアノテーションを設定するだけでOKです。

8
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
11