将Java 8的Spring Boot + QueryDSL应用程序升级至Java 11

总结

在将运行在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文件。

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 的详细介绍,但该框架使用了注解处理器工具(APT)的功能。APT 会提取 Java 类,并生成用于实现针对这些提取的类的查询的方法链的类文件。

JPA注解处理器

在build.gradle的generateQueryDSL中有一些属性,而JPAAnnotationProcessor通过options.compilerArgs属性将其定义为APT。当编译Java时,APT将以附有以下注解的类作为编译目标。

・ @实体
・ @映射超类
・ @可嵌入
・ @嵌入式

另外,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 はカンマで区切りで指定。

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

参考
JavaCompile类是Gradle API中的一个任务编译类。可以在以下链接找到详细的文档和说明:
https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/compile/JavaCompile.html

执行 generateQueryDSL

按照上述的定义,generateQueryDSL任务将被执行,并最终生成Q java文件。
例如,对于以下实体:

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

    private Integer artistId;
}

以下的 Q 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);
   // 以下省略
}

如果按照下面的结构,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。

春季引导

据官方页面显示,据说Java 9以及以上版本不支持1.5.x版本。
因此,我们需要升级到Spring Boot 2.1.1版本。

Spring Boot 2是第一个支持Java 9的版本(同时也支持Java 8)。如果你正在使用1.5版本且希望使用Java 9,你应该升级到2.0,因为我们没有计划在Spring Boot 1.5.x上支持Java 9。

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 版本,需要更改 /gradle/wrapper/gradle-wrapper.properties 中的 distributionUrl。

distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-rc-2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-all.zip

参考:
如果测试中有内部类,使用JDK 11构建时会出现问题。
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最终变成了这样的形式。

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项目,现在需要 dependency-management。
此外,还需要添加 net.ltgt.apt 插件。一旦该插件被配置,Gradle 将会在生成基于 APT 的源代码(在这种情况下是 Q java 文件)时,在以下文件夹中生成源代码。

/build/generated/source/apt 可以用中国的原生语言改写成:/构建/生成的/源码/apt

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

如果没有这个插件,在通过Gradle 使用 APT 生成源代码时,会在与 .class 文件相同的目录中生成 Q java 文件。

/build/classes/java is translated into Chinese as /构建/类/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

net.ltgt.apt
https://plugins.gradle.org/plugin/net.ltgt.apt的中文释义:

④ Java 版本

将Java版本升级至11。

将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 目录下生成的 Q java 文件将被识别为源代码。

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

⑥ APT的依赖性

根据GitHub上的问题,从Gradle 5.0开始,它不再自动从类路径获取注释处理器。
根据问题页面的描述,需要明确地将AnnotationProcessor添加到dependencies中。由于lombok也在测试类中使用,所以还需要添加testAnnotationProcessor。

在官方网页上,我们被指示为每个注解处理器分别定义Annotation Processor。因此,queryDSL和lombok的dependencies将与普通的dependencies分开定义。

由于实现细节对于注解处理器很重要,它们必须在注解处理器路径上单独声明。Gradle会忽略编译类路径上的注解处理器。

另外,对于 queryDSL 的依赖中的 AnnotationProcessor,我指定了以下三个参数。

    1. com.querydsl:querydsl-apt:${queryDSLVersion}:jpa

 

    1. javax.annotation:javax.annotation-api:1.3.2

 

    1. org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final

以下是中文翻译的选项:

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

关于第一点,需要使用QueryDSL的APT,并且在版本后面设置了”jpa”的jar引用。关于第二和第三点,如果只有第一点的描述,在通过IntelliJ进行重建和测试时是成功的,但是通过gralew编译compileJava任务时会生成失败,可能是由于Q java文件生成失败导致找不到符号而导致错误。

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

当将2和3作为参数设置给gradlew时,compileJava会正常通过gradlew完成。

参考了Gradle 5.0,对annotationProcessor进行了调查。链接如下:
https://qiita.com/opengl-8080/items/08a9cbe973fad53d93a7

在Gradle和IntelliJ IDEA中使用QueryDSL注释处理器

⑦ 在 Gradle 执行时的行为

在Gradle中执行compileJava任务时,会在build目录下生成Q java文件。在生成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文件夹下。
因此,当compileJava结束时,我们必须确保始终执行cleanUpQueryDsl任务。

编译Java任务的流程如下所示。

    1. 在开始编译Java之前,删除 out/production/classes/generated 文件夹下的所有 Q.java 文件。由于需要递归删除文件夹,因此需要指定文件对象作为参数。

 

    1. 当执行compileJava时,由于ltgt插件的帮助,会在build/generated文件夹下生成Q.java文件。

 

    1. 在compileJava结束后,将build/generated文件夹下的所有Q.java文件复制到out/production/classes/generated文件夹下。

 

    在Q.java文件复制完成后,删除不再需要的build/generated文件夹下的所有Q.java文件。

由于这个操作,与Java 1.8不同的是,不再需要在src/generated目录下生成Q Java文件。
因此,我们将删除在src/generated文件夹下生成Q Java文件的任务generateQueryDSL。

清潔時的行為。

不一定必要,但执行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 的方法。

    プロダクション
    @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);
    }
    テスト
    @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文件中添加了配置。

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

Spring Boot 2.1.x 默认禁用了对此类型的Bean的覆盖功能。请尝试在应用属性中添加以下行:
spring.main.allow-bean-definition-overriding=true
这将重新启用先前的行为。

改变 GeneratedValue 的值

使用JPA时,需要使用@GeneratedValue来指定ID的生成方法。如果在使用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

指定明”Generation Type”,会消除错误。
下面的情况,通过使用”IDENTITY”来避免错误。

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

找到一个替代findOne方法

JpaRepository 中的 findOne 方法已被弃用。需要使用 Optional findById(S) 来替代。

解决”hibernate.dialect”未设置的问题。

由于发生了下述错误,我明确地将使用的数据库添加到了application-.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会导致的难以理解的错误。故障排除变得更加容易。

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();

列表 dtos = 查询.select(
Projections.bean(UserDTO.class, user.firstName, user.lastName)).获取();

我认为不需要对 Setter 进行实现的原因是,如果在 Setter 中可以设置数据,那么在实例生成后,任何时候、任何地方都可以从外部设置数据。即使在用于没有行为的 DTO 上设置数据,也可能出现意想不到的副作用。为了使对象成为不可变对象,我认为更好的方法是不使用 Projections.bean 的方式,而是使用 Projections.constructor 来设置数据。

在构造函数中设置数据

查询语句如下:
List dtos = query.select(
Projections.constructor(UserDTO.class, user.firstName, user.lastName)).fetch();

虽然在字段上直接设置数据的方法也是可行的,但是我认为定义构造函数并隐式地设置字段值会更容易理解。如果从构造函数中设置数据,那么是否构造函数设置数据更加明显呢?

定义一个无参数的构造函数并从字段中设置数据会使对象的可用性非常差,所以这种方法是不可取的。

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

3.2. 结果处理
http://www.querydsl.com/static/querydsl/latest/reference/html/ch03s02.html

参考文献:

3.2. 结果处理
http://www.querydsl.com/static/querydsl/latest/reference/html/ch03s02.html

可以减少标注。

使用Hibernate时,需要编写与领域(业务知识)无关的注释,例如@Column或@ManyToOne。一旦停止使用Hibernate,这种规定将完全消失。只需在实体上设置实体的@Entity注解即可。

bannerAds