JOOQ-入门手册-二-

104 阅读51分钟

JOOQ 入门手册(二)

原文:Beginning jOOQ

协议:CC BY-NC-SA 4.0

四、集成 jOOQ

也被称为“吃你的蛋糕,也有它。”如果你和 jOOQ 在一个新项目中重新开始,恭喜你,祝你好运!如果你有一个包含其他技术和 API 的现有项目呢?jOOQ 仍然可以发挥巨大的作用。总而言之,jOOQ 在以下几个方面做得非常好:

  • 生成高度表达性、类型安全、可重用的 SQL ,这样您就可以放心,您的 SQL 总是正确的。

  • 从数据库实体生成 Java 类,这样您就再也不用手动构建另一个实体、DTO 或活动记录了。

  • 顺利管理数据库方言、怪癖和缺点这样您就不必为不同数据库供应商之间的差异而烦恼。顶级便携性!

…当然,还要与数据库服务器的性能和可扩展性保持一致。话虽如此,jOOQ 并不想成为你唯一的爱。我的意思是,这很好,但是如果你已经使用了某些技术,jOOQ 非常乐意分担责任。

img/500754_1_En_4_Figa_HTML.png

只要我们都明白谁是这个 真正的 这个联盟中的数据库的朋友

在这一章中,我们将看看 jOOQ 如何通过将其独特的能力借给现有的 API 和库来增强您的应用实现。

使用 jOOQ 的 Java 持久性 API

Java 持久性 API (JPA)是 JakartaEE 规范,它定义了如何将数据库对象映射到 Java,也称为对象关系映射(ORM)。它展示了实现 API 应该如何处理将数据库组件、SQL 和其他数据库内容转换成 Java 类、接口以及相反的过程。它定义了特定环境下参考实现的预期行为。它还定义了 Java 持久性查询语言(JPQL),这是一种 SQL 风格的查询语法,试图复制 SQL 的习惯用法,但用于 Java 类。然后,我们将目光投向供应商,如

  • 红帽/冬眠

  • Eclipse/EclipseLink

  • Oracle/TopLink

  • OpenJPA

最终,行业供应商将按照规范的指导实现一个功能 API。关键词是“指导”——规范是一个指导方针,供应商可以并且通常会违反规范的规则。或者,他们可以实现一些规范定义的功能,但是以非标准的方式。这意味着您的收获可能因 JPA 实现的不同而不同。

但是你可能已经知道了这一切。

这里我们不打算详细探讨 JPA 只想回答一个问题:在 JPA 的世界里,jOOQ 能为你做什么?

生成 JPA 实体

JPA 中的基本工作单元是实体。JPA 实体是一个 Java 类,用于表示数据库表或表中的行。因为这不是一本 JPA 教科书,所以我不打算详细介绍 JPA 实体。简单地说,jOOQ 可以为您创建一些基本的 JPA 实体。你只需要问:

<generate>
            <jpaAnnotations>true</jpaAnnotations>
</generate>

真的就这么简单。在 jooq 代码生成器设置中标记jpaAnnotations“on ”,您将得到如下内容:

/**
 * This class is generated by jOOQ.
 */
@Entity
@Table(
    name = "vehicle",
    schema = "edens_car",
    indexes = {
        @Index(name = "veh_manufacturer_id_idx", columnList = "vehicle_manufacturer ASC"),
        @Index(name = "veh_model_id_idx", columnList = "vehicle_model_id ASC"),
        @Index(name = "veh_style_idx", columnList = "vehicle_style ASC")
    }
)
public class Vehicle implements Serializable {

    private static final long serialVersionUID = 1L;
    private Long          vehicleId;
    private Long          vehicleManufacturer;
    private BigDecimal    vehicleCurrPrice;
    private LocalDate     vehicleModelYear;
    private String        vehicleStatus;
    private String        vehicleColor;
    private Long          vehicleModelId;
    private Long          vehicleTrim;
    private Long          vehicleStyle;
    //more fields
    public Vehicle() {}
    //getters, setters, constructors, toString etc
}

这里最重要的注释是@Entity。这对于 JPA 运行时来说意味着这个类的实例应该由 JPA 运行时来管理。这对 JPA 运行时如何看待这个类的实例有很大的影响。从这个实体类Vehicle的实例存在的那一刻起,JPA 运行时就开始关注了。实体的任何变化、它的任何新实例、从数据库的检索等等。所有这些都由 JPA 运行时跟踪。当两个线程试图修改支持特定Vehicle实例的底层表行时,JPA 运行时的工作就是确保只有一个线程或者没有一个线程成功地进行修改。

现在,我们的赞助商说一句话。

img/500754_1_En_4_Figb_HTML.png

不,不是你,先生!

我强烈主张像对待底层数据库行数据一样对待 JPA 实体的实例。像处理 POJO 或数据传输对象(DTO)这样的“哑”对象一样处理这些实体,这是一种非常普遍但阴险的代码味道。因为实体是活动的、受管理的对象,所以您面临

  1. 在正常的进程执行过程中意外地改变了对象的状态。

  2. 如果在只读操作中长时间持有实体的实例,就会引发类似StaleObjectStateException的状态管理异常。这在分布式环境和微服务中尤其容易发生。一个线程只是想读取一些数据,也许把它作为 web 服务响应发送出去。另一个线程同时想要对同一实体的底层数据进行更改。这些线程中的一个将会有一段糟糕的时间。

  3. 当您对数据库操作和 web 服务响应使用相同的实体类时,或者将它持久化为不同的格式(如 JSON)时,会泄漏数据。您将在几个方向上不加选择地传输表列。

TL;DR:将你的 POJO 需求和 ORM 需求分开。他们不是同一类型的班级。

一种解决方法是运行代码生成器两次:一次将jpaAnnotations设置为false,另一次将其设置为true。记得在两次运行之间更改输出包。

除了普通的 JPA 注释,jOOQ 还可以添加

  • Serializable<serializablePojos>true</serializablePojos>的接口

  • <jpaVersion>2.2</jpaVersion>对特定版本的 JPA 支持

近了。

从 JPA 实体生成

*是的,你没看错:jOOQ 可以让你开始使用 jOOQing,即使你没有一个真正的数据库。如果你有 JPA 实体但没有数据库,jOOQ 仍然可以为你生成代码。考虑到我反对重用实体作为 dto 或 POJOs,这非常方便。这样,您的 JPA 实体可能已经预先生成并打包成一个 JAR 您所需要做的就是从这些实体中生成 POJOs,这样您就可以轻松度日了。观察。

首先添加以下 Maven(或等效的 Gradle 等。)项目的条目:

<dependency>
         <groupId>org.jooq.pro-java-11</groupId>
         <artifactId>jooq-meta-extensions-hibernate</artifactId>
         <version>3.15.1</version>
</dependency>

这将引入 jooq hibernate 扩展包。接下来,对代码生成器本身进行一些配置更改:

<database>
       <name>org.jooq.meta.extensions.jpa.JPADatabase</name> (1)
      <properties>
      <!-- A comma separated list of Java packages, that contain your entities -->
          <property>
              <key>packages</key>
              <value>com.apress.samples.jooq.jpa.entity, com.apress.samples.jooq.ext.jpa</value> (2)
         </property>
<!-- The default schema for unqualified objects:
- public: all unqualified objects are located in the PUBLIC (upper case) schema
- none: all unqualified objects are located in the default schema (default)
This configuration can be overridden with the schema mapping feature -->
         <property>
               <key>unqualifiedSchema</key>  (3)
               <value>none</value>
        </property>
    </properties>
</database>

这是什么?

  1. 为了从 JPA 实体类生成,需要更改名称。org.jooq.meta.extensions.jpa.JPADatabase定义生成器的数据来源。将这与我到目前为止一直使用的org.jooq.meta.mysql.MySQLDatabase进行对比,因为我的代码是从实际的数据库中生成的。

  2. 我指定了 jOOQ 应该扫描的包,以便能够解析 JPA 实体类。

  3. jOOQ 应该如何处理没有模式数据的实体?unqualifiedSchema属性接受none,这意味着所有缺少模式信息的实体都将被放入默认模式中。public也是有效的,意味着默认情况下这些实体将被放在公共模式中。你也可以用SchemaMapping覆盖所有这些。

对于我反对滥用实体类作为 dto 的理由来说,这是另一个很好的变通方法,尤其是当您已经有了遗留的 JPA 实体类时。只需从现有的 JPA 实体中生成 POJOs,并跳过代码生成器配置中的jpaAnnotations指令。

生成 SQL 查询

当然,最明显的用例。jOOQ 将永远超越 JPA 能想到的任何东西。因此,当您想要认真对待您的数据库时,您应该考虑将 SQL 查询生成委托给 jOOQ,这是显而易见的。JPA 提供了许多机会来提供您自己的 SQL。考虑我们钟爱的车辆选择查询:

Query jooqQuery = DSL.using(SQLDialect.MYSQL,new Settings()
                .withRenderQuotedNames(RenderQuotedNames.NEVER))
                .select(VEHICLE.VEHICLE_ID, VEHICLE.VEHICLE_COLOR, VEHICLE.VEHICLE_CURR_PRICE)
                .from(VEHICLE)
                .where(VEHICLE.VEHICLE_MANUFACTURER.eq(param("vehicle_manufacturer", Long.class))).getQuery();

前面的 jOOQ 语句

  • 使用DSL类配置即将生成的 SQL 语句的方言。它还指定引号不应该用在生成的 SQL 中——这可能变得很重要,这取决于为 JPA 实现配置的方言。例如,常规的双引号(")可能会导致 Hibernate 阻塞。

  • vehicle表中选择一些字段,但是我没有执行它,而是获得了org.jooq.Query的一个实例。这是所有 SQL 语句的 jOOQ 表示的父接口。你注意到我在这里没有使用DSLContext了吗?相反,我直接使用DSL类来创建我的 select 语句。这意味着我不需要为了构建 jOOQ SQL 查询而去构建DSLContext或 JDBC 连接。

  • VEHICLE.MANUFACTURER列作为查询参数与param函数绑定。这意味着我可以在运行时提供一个动态值。

  • 最后的getQuery方法产生了一个Query对象,从这个对象中我可以获得纯文本 SQL 语句,以及其他内容。

这对 JPA 世界有什么帮助?

JPA 为您提供了一系列机会来提供自己的 SQL 查询。你为什么想这么做?嗯,简单的事实是,对于任何比来自几个表的简单的SELECT语句更复杂的东西,JPA 都不是最好的选择,特别是在大规模的情况下。如果需要使用常用的表表达式、内联视图、窗口函数等。,您将需要创建自己的 SQL。JPA 菜单上没有分层查询。JPQL 尽可能地支持 SQL 规范的一个子集。这就是你的Query对象出现的地方。

@PersistenceContext
 EntityManager entityManager;  (1)
...
javax.persistence.Query nativeQuery = entityManager.createNativeQuery(jooqQuery.getSQL());  (2)
int parameterCount = 1; //JDBC parameter values begin their index at 1, not 0.
 long vehicleStyle = 4;
        for(Parameter parameter: nativeQuery.getParameters()){
             nativeQuery.setParameter(parameterCount++, vehicleStyle); (3)
        }
List<Vehicle> resultList = nativeQuery.getResultList(); (4)
logger.info("Results: count: {} \n list: {}",resultList.size(),resultList.toArray());

那好吧。让我们深入了解一下:

  1. EntityManager是进入 JPA 运行时的网关,也称为PersistenceContext。我提到过我对上下文对象模式有多着迷吗?这是其中之一。从数据库映射的所有数据库行都可以从该对象中获得。几乎所有您想用 JPA 做的事情都可以从这里开始。根据您使用的平台(JakartaEE、Spring Data、Quarkus 等),有多种方法可以获得这个对象的实例。);我不会在这里详细讨论。

  2. EntityManager对象提供了允许我提供定制 SQL 的createNativeQuery方法。这就是我的org.jooq.Query物体发光的地方。我使用getSQL方法获得从 jOOQ 查询中生成的明文 SQL。

  3. 因为我已经在 jOOQ 查询上定义了一个查询参数,所以 JPA 查询通过我传递给它的普通字符串 SQL 自动继承了这个参数。这意味着我可以动态地为 JPA Query对象识别的每个可用的Parameter设置值。总的来说,这是一个特别灵活的操作,例如:除了索引值之外,我还可以通过名称引用我的查询参数。

  4. 最后,我可以执行 SQL 语句并使用Query#getResultList检索我的查询结果。这个方法可以返回一个 JPA 实体类列表或一个Object列表,我可以透明地将它们转换成我选择的任何类。这里,我选择使用 jOOQ 为我生成的Vehicle POJO 类。这是一个非附加的、非托管的 java 对象,所以我不必担心通过这个查询的结果意外修改底层数据库数据。

在 JPA 世界中,还有其他机会来利用来源可靠、经过认证的无冲突和无麸质 SQL。您可以将一个 JPA 实体类传递给createNativeQueryMethod

Query nativeQuery = entityManager.createNativeQuery(jooqQuery.getSQL(), VehicleEntity.class)

使用这种方法,EntityManager返回的任何VehicleEntity实例都是托管对象——如果对这些对象的状态进行更改,将会影响底层 db 行中的数据。这将把查询结果映射到该类的实例,前提是列名和其他内容匹配。

当数据库列与您的类声明不一致时怎么办?也许您正在使用列别名,或者您想从一个语句中返回多个实体类型?当你根本不想使用 JPA 实体类的时候怎么办?就我个人而言,我绝对喜欢不用担心通过实体意外修改表数据。我希望断开连接的对象用于只读目的。

看,SQL 结果映射的三个 JPA 骑手!

img/500754_1_En_4_Figc_HTML.png

"l ol 什么是 SQL?"

好吧,说真的,是这三个注解:

  1. @SqlResultSetMapping定义是否需要将 SQL 查询的结果映射到 java 对象。这个注释可以应用于任何带有Entity注释的 JPA 类。在一个类上定义了这个注释之后,你可以通过名字来引用它。请继续关注我,看看它的实际应用。

  2. 在 JPA 2.1 中引入,这样我们可以使用 JPA 来构造非托管的 Java 对象/实体。在此之前,一切都必须是一个托管的 JPA 实体。见我之前的警告,为什么这可能成为一件坏事。有了这个注释,即使你提供了一个用@Entity注释的类,JPA 运行时也会忽略它,并且不会试图管理这个构造的任何结果。

  3. @ColumnMapping允许您将 SQL 查询结果中的列映射到非 JPA 实体(即 POJO)的字段。这就是如何定义从 SQL 结果到 Java 类字段的列别名和其他不一致名称的映射。在 JPA 行话中,这样的列被称为标量列

那么,这些是如何协同工作的呢?看看这个:假设我已经运行了 jOOQ 生成器并获得了一个 JPA 注释的 POJO com.apress.jooq.generated.tables.pojos.VehicleModel,我可以让映射注释像这样工作:

@SqlResultSetMapping(name="nonJPAManagedVehicleModel", (1)
        classes = {
        @ConstructorResult(targetClass =  com.apress.jooq.generated.tables.pojos.VehicleModel.class,   (2)
        columns = {                         (3)
                            @ColumnResult(name="vehicle_model_id"),
                            @ColumnResult(name="vehicle_model_name"),
                            @ColumnResult(name="vehicle_style_id"),
                            @ColumnResult(name="vehicle_man_id"),
                            @ColumnResult(name="version")
                             })
       })
@Entity
public class VehicleModel implements Serializable {
...
}

好吧,系好安全带,我来解释这里发生了什么:

  1. 一切都从这里的@SqlResultSetMapping开始,我说:“我想在 SQL 语句和 POJO 之间定义一个自定义映射。我已经命名为查询nonJPAManagedVehicleModel,因为我就是这么做的。”

  2. 然后我定义这个自定义映射中涉及的类。对于这个例子,我只对 POJO VehicleModel感兴趣。这就是事情变得有点冗长的地方。

    1. 我需要为 JPA 运行时描述一个合适的构造函数,以便能够用@ConstructorResult创建我的 POJO 类的实例。有了这种映射,JPA 知道如何处理查询结果。

    2. 请记住:尽管这个类在技术上是一个 JPA 实体类,但是当我在这个上下文中使用它时,JPA 不会将这个查询的结果视为托管实体,这在我看来是非常棒的。

  3. @ColumnResult帮助我将查询结果中的name映射到VehicleModel POJO 类中的字段。JPA 如何知道将列映射到类中的哪个字段呢?由列表columns中列的位置决定。JPA 运行时将寻找一个合适的与这里的描述相匹配的构造函数,并只挑选指定的列传递给构造函数。

最后,我可以像这样使用我的命名查询:

entityManager.createNativeQuery(vehicleModelQuery.getSQL(),"nonJPAManagedVehicleModel");

JPA 运行时将尝试使用我提供的名称来查看我的 SQL 映射。这为它提供了执行查询和构建结果对象列表所需的所有信息。

这是相当冗长的,所以不要担心它是否能一下子完全理解——尽可能多的重复一遍。更简单的描述是告诉 JPA

  1. 要映射到哪个 POJO 或实体类

  2. 应该使用 SQL 结果集中的哪些列名

  3. 在 POJO 类上使用哪个构造函数

  4. POJO 类的构造函数中应该使用哪些列

总之,这些工具允许您将 SQL 查询打包到非常可移植和灵活的部署单元中;考虑数据库方言,并保证查询的有效性。

Caution

在撰写本文时,jOOQ 有一个错误 1 ,使得它需要对 JPA 实体进行@Column注释。jOOQ 生成的实体类不会有这个问题;但是如果您将自己的 JPA 实体带到 jOOQ 聚会,请确保将@Column添加到该实体的字段中。否则会发生一些奇怪的事情(例如,列值没有映射到结果对象)。

现在,我们向结果集映射的骑手说再见。

img/500754_1_En_4_Figd_HTML.png

Aww!振作起来,伙计们!

Spring Boot 和乔克

Spring Boot 是当前企业 Java 开发的领军人物。电流。几乎没有什么是你不能用 Spring 平台做的,我甚至不打算在这里讨论它的许多特性。让我们看看 jOOQ 如何美化您的 Spring Boot 应用。但首先,一些配置:

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost/edens_car
spring.datasource.username=username
spring.datasource.password=thisisaterriblepassword
spring.jpa.show-sql=true

这为我的 Spring Boot 应用设置了到 MySQL 数据库的连接属性。这些属性放在标准的application.properties文件中。还有编程上的对等物。

Spring 通过以下组件支持 SQL 数据访问:

  • 香草 JDBC 访问的春季数据 JDBC

  • spring Data JPA for the sweet Hibernate+JPA combo

  • 用于反应式数据访问的 Spring 数据 R2DBC

因为 Spring Boot 平台是如此的庞大,我将在这一部分尽量保持简洁。还要注意,我们在上一节以及这一节中介绍的几乎所有内容都适用于 Hibernate。Hibernate 是 JPA 实现,它为 Spring framework 的许多数据访问功能提供了动力。

首先要知道的是,你可以用 Spring Boot 批量安装 jOOQ 作为你的整个数据访问组件。大概是这样的。

在 Spring Boot 配置 jOOQ

让我们通过编程配置设置使 jOOQ 在 Spring 应用上下文中随处可用:

@Configuration                                               (1)
@EnableTransactionManagement
public class JdbcConfig extends AbstractJdbcConfiguration {
    @Autowired
    private DataSource dataSource;                (2)

    @Bean
    DataSourceConnectionProvider connectionProvider() {
        return new DataSourceConnectionProvider(new TransactionAwareDataSourceProxy(dataSource));
    }

    @Bean
    DSLContext dsl() {
        return new DefaultDSLContext(dslConfig());         (3)
    }

    private org.jooq.Configuration dslConfig() {

        DefaultConfiguration defaultConfiguration = new DefaultConfiguration();
        defaultConfiguration.set(dataSource)
                .set(SQLDialect.MYSQL)
                .set(DefaultExecuteListenerProvider.providers(new QueryRuntimeListener()));
        return defaultConfiguration;
    }
}

这里有相当多的 Spring 框架样板文件,但是我将把重点放在与 jOOQ 相关的部分:

  1. 我用@Configuration注释和其他标准的 Spring 框架组件(如@EnableTransactionManagement)设置了我的 Spring 配置 bean,让 Spring 管理我的数据库事务;AbstractJdbcConfiguration所以我的配置类可以继承更多的样板文件。这是样板大杂烩。

  2. 我使用 Spring 的依赖注入来获得一个DataSource实例。DataSource是我的数据库连接和池的更成熟、可伸缩和健壮的表示,由 Spring Boot 运行时管理。这将在这里提供,因为我已经在标准的application.properties文件中配置了我的数据库属性。

  3. 我定义了一个可以按需构造DSLContext实例的方法。添加@Bean注释将它标记为 Spring Boot 的工厂方法。这意味着我可以在我的 Spring 应用中的任何地方获得一个新的DSLContext实例。

有了这个设置,我可以在应用的任何地方获得一个DSLContext:

@Autowired
DSLContext context;

public void selectWithJooq(){
    context.selectOne();
}

然后我就可以尽情地狂欢了。可以扩展前面的代码片段,为每个请求生成一个新的DSLContext实例,支持多租户等等。如果你能梦想到,jOOQ 可能会尽最大努力去实现它。更不用说 jOOQ 能为你生成的 Dao 了。不错。

自定义 SQL 查询怎么样?

使用自定义 SQL

如果有定制的 SQL 需要编写,jOOQ 会自动生成。为了使用 Spring Data JPA 的定制查询,我首先创建一个Repository:

public interface VehicleModelJooqRepository extends CrudRepository<VehicleModel, Long> { (1)
    @Query(nativeQuery = true, name="CustomDynamicSQL")
    List<VehicleModel> findVehicleModelByVehicleManId(long id);
}

请允许我解释:

  1. 我扩展了CrudRepository,作为使用 Spring Data JPA 的存储库特性的合同的一部分。将VehicleLong指定为该接口的类型,我将通知 Spring 数据运行时,该接口将用于从vehicle_model表中检索VehicleModel

  2. 我定义了一个findVehicleModelByVehicleManId,它接受一个对应于vehicle_man_idlong参数来过滤结果

    1. 重要的是,我使用了@org.springframework.data.jpa.repository.Query注释。Spring Data JPA 允许我在这个注释中指定一个明文 SQL 查询;或者,我可以在其他地方定义查询,通过一些 Spring 魔法,它将被选中。敬请关注。到目前为止,这个 JPA 存储库期望在PersistenceContext的某个地方找到一个名为“CustomDynamicSQL”的原生查询。

    2. 我传递给查询方法findVehicleModelByVehicleManId的每个参数都将作为查询参数传递给该方法将要执行的本地查询。这一点很重要,因为要么必须将方法参数的位置与普通 SQL 中查询参数的位置相匹配;或者,您可以使用@Param注释将您的参数与其 SQL 等价物进行名称匹配。

现在,我需要连接我的定制 SQL 查询,由 jOOQ 赞助。为了将我的 SQL 查询真正插入 JPA 运行时,我求助于我们的老朋友,EntityManager:

javax.persistence.Query nativeQuery = entityManager.createNativeQuery(jooqQuery.getSQL());
entityManager.getEntityManagerFactory().addNamedQuery("CustomDynamicSQL",nativeQuery);

从我的org.jooq.Query中获得了javax.persistence.Query的实例:

  1. 我从EntityManager中获得一个EntityManagerFactory

  2. JPA 2.1 中添加了addNamedQuery方法,允许动态构造命名查询。有了这个,我需要补给

    1. Spring Data JPA 可以通过其查找的查询的名称。注意我是如何使用我在前面的Repository接口中定义的方法的完全限定名的。这就是 Spring Data JPA 将如何尝试基于我添加到我的自定义Repository方法中的@Query注释来查找命名查询。

    2. 要执行的实际 SQL 查询。

剩下的就交给 Spring Boot 了。我只需注入我的自定义存储库,并根据需要使用它:

@Autowired
VehicleModelJooqRepository vehicleModelRepository;
...
List<VehicleModel> modelByVehicleManId = vehicleModelRepository.getVehicleModelByVehicleManId(vehicleManufacturer);

…就这样!这个动态 SQL 特性是对我们已经探索过的标准 JPA 特性的补充——Spring 也支持这些特性。

jOOQ Spring Boot 首发

据说 Spring Boot 提供了一个入门工具,可以帮助你用 jOOQ 引导你的 Boot 项目。

img/500754_1_En_4_Fige_HTML.jpg

start.spring.io

在实践中,我试图用它来引导,包括 Spring 数据 JPA 和 JDBC 模块。对我来说看起来不是很有效,因为

  • starter(目前)不包含任何 jOOQ 依赖项。

  • 它生成的代码存根甚至不包含任何对 jOOQ 的引用(参见前面的讨论)。

所以,也许暂时不要谈这个。

夸特斯和乔克

Quarkus 2首屈一指的云原生、容器和 Kubernetes 优先的微服务平台。它几乎支持你想用 Java web 服务平台做的任何事情。您可以集成现有的 JakartaEE 或 Spring beans,并使用相同的编程组件来获得

** 极快的启动时间

  • 低内存占用

  • 与 AWS、Google Cloud 和 Azure 等主要云提供商的功能和组件紧密集成

  • 轻量级部署包

  • 反应优先的编程风格

  • Kotlin 和 Scala 兼容性

Quarkus 确实是软件工程的天赐之物。我这么说是作为一个完全独立和公正的观察者。

img/500754_1_En_4_Figf_HTML.jpg

是的。完全无偏

那么,jOOQ 在夸库斯能为你做什么?就像 Spring Boot 一样,jOOQ 可能是您需要的所有 SQL 数据访问。它还可以与 Quarkus 中现有的 API 一起工作,比如

  • 作业的装配区(JobPackArea)

  • 冬眠

  • 反应式 SQL

  • SQL 结果集流

在撰写本文时,您唯一不能做的事情是在native模式下使用@Query注释。Quarkus 支持大部分 Spring 数据 JPA,除了这个位。那么,怎样才能让乔克进入夸库斯呢?

从 Quarkus jOOQ 扩展开始:

<dependencyManagement>
   <dependencies>
      <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-bom</artifactId>
        <version>${quarkus.platform.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
<dependency>
      <groupId>io.quarkiverse.jooq</groupId>
      <artifactId>quarkus-jooq</artifactId>
      <version>0.2.2</version>
</dependency>

除了标准的 jOOQ 依赖项之外,前面的代码片段摘录了将 jOOQ 扩展添加到 Quarkus 需要添加到 Maven POM(或 Gradle 等价物)中的内容。这不一定是向 quarkus 添加扩展的最佳方式。理想情况下,您应该像这样使用 Quarkus maven 插件:

mvn quarkus:add-extension -Dextension=quarkus-jooq

这是安装 Quarkus 扩展的标准方式。它不适用于 jOOQ 扩展,因为它(还)不在 Quarkus 扩展的注册表中。jOOQ 扩展不是 Quarkus 的“官方”扩展,因为它不是由 Quarkus 核心团队构建和维护的。这是他们称之为 Quarkiverse 的一部分, 3 一个扩展的扩展生态系统,将所有权交给开发者社区。Quarkus-jOOQ 扩展是 Quarkus 团队之外的个人在大众需求的支持下努力工作的结果。大声喊出来!

接下来,为您选择的数据库安装合适的 JDBC 扩展:

mvn quarkus:add-extension -Dextension=jdbc-mysql

安装 Quarkus 扩展后,您可以在 Quarkus 应用的application.properties文件中配置您的数据源设置:

quarkus.datasource.db-kind=mysql
quarkus.datasource.username=dbuser
quarkus.datasource.password=thisisaterriblepassword
quarkus.datasource.jdbc.url= jdbc:mysql://localhost/edens_car
quarkus.jooq.dialect=mysql

要使这些工作正常进行,必须配置方言。有了这个基本配置,您可以在应用的任何地方获得一个DSLContext:

@Inject
DSLContext dslContext;

@Inject注释是 Spring 框架中@Autowired注释的上下文和依赖注入(CDI)等价物。Quarkus 出于同样的目的支持这两者。quarkus-jooq 扩展只在 jooq 的社区版中提供。如果 POM.xml 中有专业版,可以用专业版覆盖社区版。

现在,让我们谈谈包装和测试所有这些东西。

Footnotes 1

https://github.com/jOOQ/jOOQ/issues/4586

  2

https://quarkus.io

  3

https://github.com/quarkiverse/quarkiverse/wiki

  4

https://github.com/quarkiverse/quarkus-jooq

 

**

五、封装和测试 jOOQ

最后冲刺!让我们通过谈论 jOOQ 如何适应“现代”软件开发主题,如持续集成/持续开发(CI/CD)、容器(Docker、Podman 等)来结束 jOOQ 路演。),等等。

但首先,在我们进入肉类(或者蔬菜,如果你喜欢的话)之前,让我们先做一点水平设定:

  • jOOQ 允许你自带 SQL (BYOS)。

  • jOOQ 将为您生成代码,您在编译时最可能需要的代码。

  • 你生成的代码成为你的业务逻辑的一部分,去做…事情。

在当今世界,你需要

  • 能够验证您的定制 SQL 是否有效——无论是您自己编写的 SQL,还是由另一个开发人员或团队打包并交给您的 SQL。

  • 能够管理对您的数据模型所做的增量更改——由您自己或您组织的其他部分发起的更改。您将如何支持添加到数据模型中的新表或列?

  • 对您生成的代码如何生存以及在哪里生存做出实际有效的决定。您的实体和 dto 被打包在一个单独的 JAR 文件中,并作为一个依赖项包含在多个软件项目中,这种情况并不少见(甚至可能是首选)。

  • 运行集成测试,在测试时不需要整个独立的数据库服务器。以 Jenkins 构建服务器为例:理想的情况是,您的构建工作不需要一个常设的 MySQL 服务器来运行您的集成测试。

那么,当您需要…

用 jOOQ 封装代码

抱歉,这将是以 Maven 为中心的。

我们已经看到了如何使用 jOOQ Maven 插件从命令行以编程方式生成 jOOQ 代码。我们还没有谈到的是在哪里放置生成的代码。

从 Maven 的角度来看,src/target/generated-sources是生成代码的推荐位置,不管是 jOOQ 还是其他什么。假设你已经在 POM.xml 中配置了jooq-codegen插件,就像我在第三章中演示的那样,运行mvn package -DskipTests=true将会

  • 按照jooq-configuration.xml中的配置连接到数据库

  • 生成必要的代码

  • 编译整个工具包

  • 跳过运行测试

  • target目录下生成一个 JAR 文件

让我们考虑几个场景,在这些场景中,您可能想要稍微偏离这条路径。

当您不需要代码生成时

代码生成很好,但有时,您只想构建自己的工具包,跳过代码生成这一步。也许你已经生成了一次代码,什么都没有改变;或者您有一个不想现在处理的大型模式;或者您正在不支持代码生成器的环境中运行构建。像这样配置 Maven 概要文件:

<profiles>
        <profile>
            <id>no-jooqing</id> (1)
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.jooq</groupId>
                        <artifactId>jooq-codegen-maven</artifactId>
                        <version>3.15.1</version>
                        <executions>
                            <execution>
                                <id>jooq-codegen</id>
                                <phase>none</phase> (2)
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

这是一个标准的 Maven 配置文件配置,您可以在 POM 中的任何位置将其作为顶级元素添加进来。我在这里做的是

  1. 配置名为no-jooqing的配置文件。在这个概要文件中,我定义了 jOOQ 代码生成器插件的基础。这个配置片段反映了 POM 的build部分中相同插件的配置。这里的想法是让这个绑定到概要文件的定义覆盖另一个主插件定义。

  2. 我将它的阶段执行设置为none,这意味着这个插件不应该在任何时候启动。

有了这个设置,我就可以运行 maven 构建了,如下所示:

mvn package -DskipTests=true -P no-jooqing

-P标志通过名称no-jooqing激活我的档案,从而抑制代码生成器插件。可以说,有更简单的方法可以达到这种效果,但是概要文件提供了最全面的方法来选择性地执行插件。例如,您可以选择运行不同的 jOOQ 生成器配置,比如说,基于构建环境中安装的 JDK:

<profile>
    <activation>
      <jdk>14</jdk>
    </activation>
    ...
  </profile>

使用前面的代码片段,我已经配置了我的概要文件,只有当构建在 JDK 14 上运行时才生效,这是第一个为 Records API 提供官方支持的 JDK 版本(jOOQ 可以生成 POJOs)。您可以根据操作系统环境变量和其他条件激活配置文件。这确实是最强大的选择。很酷吧?

当您没有活动的数据库连接时

它发生了:您想要生成 jOOQ 代码,但是您没有访问底层数据库服务器的权限来在构建时连接。但是幸运的是,您有描述模式的数据定义语言(DDL)。jOOQ 提供了org.jooq.meta.extensions.ddl.DDLDatabase生成器组件,因此您可以直接从.sql脚本生成代码。看看这个:

<generator>
    <database>
        <name>org.jooq.meta.extensions.ddl.DDLDatabase</name>
        <properties>
            <property>
            <key>scripts</key>
            <value>src/main/resources/db-dump.sql</value>
       </property>
      ...
   <database>
<generator>

scripts属性接受一个 DDL 脚本的路径,该脚本将被加载用于代码生成。这样,您就不会在构建时被束缚在数据库服务器上。我应该提到,这个特性不仅限于打包使用——您可以在任何适合的场景中使用它。

Pro Tip

使用-- [jooq ignore start]-- [jooq ignore stop]来包装应该在 DDL 脚本中忽略的 SQL。这意味着如果你的 DDL 包含-- [jooq ignore start] CREATE TABLE ignore_me_please ... -- [jooq ignore stop] CREATE TABLE business_as_usual ...CREATE TABLE ignore_me_please会被DDLDatabase忽略。

当您的模式需要增量进化时

你听说过进化数据库模式吗?它的基本论点是:对你的数据库模式进行增量修改,就像你已经对代码做的那样。无论您是从一个全新的空数据库开始,还是已经有了一个模式,您通常都会有一个工具

  • 能够对数据模型(DDL)或原始数据(DML)应用新的更改

  • 保留已应用更改的历史记录,为回滚不兼容或中断的更改提供空间

  • 支持对应用于数据库的更改进行版本控制

  • 帮助您的代码与它所依赖的数据库保持一致

目前这一领域最大的两家公司是

它们都基于相同的基本前提:

  1. 以合适的文件格式提供您的数据库更改,以及相关的版本信息。

  2. 他们会将您的数据库更改应用到您指定的数据库。

jOOQ 是如何影响这一切的?与 Hibernates 和 JPAs 相比,jOOQ 与数据模式的状态和代码生成有着更紧密的联系。您最不希望生成的代码引用不再存在的触发器或函数。

jOOQ 通过org.jooq.meta.extensions.liquibase.LiquibaseDatabasejooq-meta-extensions-liquibase Maven 工件为 Liquibase 提供了本地支持。我自己是一个 Flyway 人,很大程度上是因为 Flyway 不需要专门的配置语法或 DSL 而且,我是一个战略上懒惰的人。

对于基本的 Flyway 用法,只需为您的.sql文件提供类似如下的版本格式:

V1__Your_Descriptive_File_Name_Here.SQL

文件名的V1部分是关键。对模式的后续更新应该增加版本号,以支持 Flyway 的增量更改机制。将所有 SQL 文件保存在/src/main/resources/db/migration中,您就可以开始工作了。此时,您应该将 Flyway 依赖项添加到 POM.xml 中:

<dependency>
       <groupId>org.flywaydb</groupId>
       <artifactId>flyway-core</artifactId>
       <version>7.14.0</version>
 </dependency>

准备就绪后,您就可以运行 Flyway 了。您可以选择命令行方法、容器化方法(稍后将详细介绍)或编程方法。让我们来看看编程方法:

Flyway flyway = Flyway.configure().dataSource(jdbcUrl,dbUser , dbPassword).load();
flyway.migrate();

真的就这么简单。Flyway 将在/db/migration中寻找最新版本的 SQL 脚本,并将更改应用到您指向的数据库。它还考虑了您的模式的先前版本,因此当您有一个V10__my_schema_update.sql时,到那时为止的更改都被考虑在内。它支持为您的迁移建立基线,因此您可以选择在V7__new_db_baseline.sql为您的模式建立基线,并且它将从那里开始考虑迁移。

顺便说一下,到目前为止,我所说的关于 Flyway 的一切都是超级可配置的;出于演示的目的,我坚持使用默认值。因为飞行路线不一定是这部分的重点。不,在这里,我想让我们考虑一个进化的数据库模型如何支持 jOOQ 的目标,以帮助生成和打包基于最新但不断进化的模式的最新代码。当考虑在 CI/CD、DevOps 繁重的环境中运行时,情况变得更加棘手。你不能指望总是有一个常设的数据库服务器连接到你的 Jenkins 主机,让 Flyway 或 jOOQ 来运行。

理想的设置是一个自包含的软件项目,它可以在项目生命周期的任何时候在内部运行自己的代码生成。无论是在开发人员的机器上,在代码库中的预合并步骤中,还是作为构建管道的一部分。不需要确保某个数据库服务器已启动。不必担心另一个开发人员对同一个数据库进行并发更改会破坏模式或数据库。是啊,那会很甜蜜,不是吗?

img/500754_1_En_5_Figa_HTML.png

极乐的

在代码中实现自给自足的一种方法是使用一个名为 TestContainers 的工具包。在这一章的后面,我会更详细地讨论测试容器。现在,可以说 TestContainers 是始终拥有一个完整的数据库并与应用捆绑在一起的最佳方式。

自给自足的数据库项目的秘诀

声明:这是一个黑客。比方说,在没有专用的 TestContainers maven 插件的情况下,您需要发挥创造力,以便能够在测试阶段之外运行 TestContainers 支持的项目。

但是在我们看到 TestContainers 如何交付一个真正自我维持的应用之前,我们应该看看我如何打包我的项目来支持我的雄心壮志。考虑下面的类:

public class PreflightOperations {
    final static Logger logger = LoggerFactory.getLogger(PreflightOperations.class);

    public static void main(String[] args){
       logger.info("Running preflight operations");
       GenericContainer container = startDatabaseContainer(); (1)
       runFlywayMigrations(container); (2)
       generateJooqCode(container); (3)
    }
}

这几乎是一个普通的 Java 类,它将做三件事:

  1. 启动一个 TestContainers 数据库(我将在本章后面展示它是如何工作的)。从启动的数据库容器中,我将能够获得一个数据库连接。

  2. 使用 observer 数据库连接,我应该能够立即运行我的 Flyway 迁移。

  3. 一旦我的模式更新被应用到我的数据库容器中,我就可以以编程方式运行 jOOQ 代码生成器。

很简单,对吧?现在的问题是:我如何让这个定制代码作为构建过程的一部分运行?Maven?Maven:

<plugin>
        <artifactId>maven-compiler-plugin</artifactId>
         <version>3.8.1</version>
                <executions>
                    <execution>
                        <id>pre-compile</id>
                        <phase>generate-sources</phase> (1)
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.0.0</version>
                <executions>
                    <execution>
                        <phase>process-sources</phase> (2)
                        <goals>
                            <goal>java</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>

                    <executable>java</executable>
                    <mainClass>com.apress.jooq.generator.PreflightOperations</mainClass> (2a)
                   <cleanupDaemonThreads>false</cleanupDaemonThreads>
                </configuration>
            </plugin>

作为构建过程的一部分,运行 java 代码的能力依赖于两个 Maven 插件:

  • maven 编译器

    这个插件将编译源代码。因为我的PreflightOperations类仍然是原始源代码,所以我需要在能够作为构建过程的一部分运行它之前编译它。

  • 玛文行政长官

    这个插件将运行任何任意可执行文件。为参数executable选择java,准备插件使用main方法执行 Java 类。

这些插件一起唱出美妙的音乐:

  1. maven-compiler 首先在 maven 构建过程的generate-sources阶段编译我的源代码。这将确保我有一个编译好的PreflightOperations类来运行…

  2. maven-exec,允许我运行任意可执行文件的插件。我选择在process-sources阶段运行这个插件,它紧接在generate-sources阶段之后。此时,数据库将启动,我的 Flyway 迁移将执行,然后 jOOQ 将生成任何必要的新源代码。

    1. 我向执行的类提供完全限定的类名(FQCN)。

    2. 因为 TestContainers 在守护线程上做了大量的后台处理,所以当 maven-exec 插件准备好继续运行时,它可能还没有准备好退出。允许构建过程继续进行,同时 TestContainers 做它自己的事情。

很简单。我想重申:这是黑客。黄金标准应该是只包含定制代码和配置。此外,生成的代码/实体通常会受益于更多的模块化。

这个配方中唯一缺少的是实际的动态实例化数据库本身。我们将在讨论…时了解这一点

用 jOOQ 测试

又名:晚上睡个好觉。我是自动化测试的绝对狂热分子,尤其是集成测试。

哦哦。我刚刚用了一个流行词:“集成测试。”集成测试往往与一堆其他不符合标准的东西混为一谈(以我外行的观点来看)。请允许我发表意见。

我相信业界已经确定了单元测试的范围,即验证独立代码单元的行为,例如,函数或方法。您不关心这些功能单元如何相互作用来交付业务场景。你可能会模仿被测方法的每一个依赖项,只关注花括号里的内容。

然后,我们有“端到端”测试,在这种测试中,您跨越了多个系统边界——前端到后端集成层,等等。这就是一些人所说的“QA”测试——确保所有东西一起工作来满足用户的需求。

在单元测试和端到端测试中间的某个地方,你会发现集成测试和有时关于它实际上意味着什么的激烈辩论。

img/500754_1_En_5_Figb_HTML.jpg

如图:集成测试。可能吧。

就本节而言,集成测试是如何确保精心选择的代码片段能够很好地协同工作。在典型的集成测试中,您会希望将一些组件连接在一起,并查看它们是否都按照您预期的方式运行。理想情况下,您的集成测试与业务/用户期望的用例紧密结合。 2

这并不是说 jOOQ 没有工具来支持单元测试——远非如此。我个人更倾向于集成测试,这让我对我要推出的产品更有信心(相对于围绕测试的虚荣度量)。问问肯特·贝克 3 他对为了测试而写测试是什么感觉。

基于 jOOQ 提供的特性,有什么需要测试的呢?

  • you 带给 jOOQ 的纯文本 SQL 语句的语法正确性。jOOQ 自己的 SQL 不太可能是错误的。

  • 你的和 jOOQ 自己的 SQL 的语义正确性。jOOQ 保护你不写语法错误的 SQL。验证 SQL 的语义正确性仍然是一个好主意,不管是生成的还是其他的。

  • 您生成的代码相对于数据库模式的准确反映。不管出于什么原因,如果您生成的代码与底层模式有一点不同步,您都不会有好的结果。

SQL 测试行业的工具

让我们看看测试 SQL 有哪些方法。需要说明的是,这些不仅仅是针对 SQL 测试的,但是你知道,这是一本 SQL-in-Java 的书,所以…

  1. JUnit ( www.junit.org

    Java 中最重要的测试——所有其他的都是装腔作势。 4 JUnit 5(代号 Jupiter)是满足您所有测试需求的一站式商店。最新版本几乎支持你能想象到的所有测试范式:行为驱动开发(BDD),验收测试驱动开发(ATDD),单元和集成测试,等等。它附带了一套注释,为在不同粒度级别测试 Java 代码提供了各种便利。但是你可能已经听说过了。

  2. 嘲弄框架

    一个模仿框架(例如,Mockito,PowerMock)将帮助你找出代码的不同部分——我知道这不是一个新概念。测试时删除或嘲笑代码中的选定部分,可以让您将测试重点放在对您来说重要的事情上。事情可能变得棘手的地方是必须使用 jOOQ 的一些静态方法。在我们深入研究这些问题时,请耐心等待。

  3. 嵌入式数据库

    In the course of testing, you’ll eventually need to be able to dynamically

    • 按需将模式加载到数据库中

    • 按需加载/销毁数据库中的数据

    • 作为测试场景的一部分,顺序运行依赖于共享状态的测试方法

      All of these scenarios require that your software project have a database ready quickly and flexibly. That’s where the embedded or “in-memory” databases come in. They’re databases that are designed for dynamic and flexible usage in lightweight scenarios, for example, testing. Examples of these include

    • H2 ( https://h2database.com/html/main.html ”)

    • HSQLDB ( http://hsqldb.org/

    • 德比( https://db.apache.org/derby/ )

    有,都是用 Java 写的。有了这些,您就可以在开发生命周期的任何时候拥有一个可用的数据库“服务器”,而不需要在任何地方部署实际的数据库服务器。

    因为它们很轻,所以它们的能力有限。因此,你通常会错过一些基本的功能。检查约束、触发器,甚至是 SQL 关键字LIMIT都可能不被支持,这取决于您选择的供应商。它们之所以轻量级是有充分理由的:快速、高效的数据库操作,没有“多余的东西”如果您更喜欢使用全功能的和便携式的数据库进行测试,那么您应该转向…

  4. 集装箱化数据库

    您可以以与 Docker、Podman 和其他容器运行时兼容的容器化格式获得最强大的数据库,如 MySQL、PostgreSQL 和 Oracle。什么是容器?当我们谈到这一点时,我会更详细地介绍,但是现在这样说就足够了:容器是你最喜欢的软件的可移植版本,打包在所谓的图像中。这些便携包通常包含完整的操作系统安装以及所有的附件;然后,您需要的软件可以与这些完整的操作系统捆绑在一起,并通过一个集中的注册中心交付。容器化的数据库(大部分)提供了首选数据库服务器的全部功能,同时保持了足够的可移植性,可以通过编程/动态地启动一个实例。这样,您就可以在任何需要的时候拥有完整的数据库,例如,作为管道、构建脚本或 JUnit 集成测试的一部分。不要半斤八两。

  5. 乔克检测试剂盒

    There are a few components in the jOOQ toolkit that support your testing and validation needs. Check it out:

    • org.jooq.tools.jdbc.MockConnectionorg.jooq.tools.jdbc.MockDataProvider和几个相关的Mock*组件有助于模拟 jOOQ 中查询操作的不同部分。

    • org.jooq.Parser可用于验证您的 SQL 查询,尝试从您的明文 SQL 中产生 jOOQ 工件。

使用 jOOQ 编程时,上述工具的不同组合会让您安心。更不用说 Spring framework 和 Quarkus 等生态系统提供的各种测试工具了——它们都有许多强大的测试技术。最终,我在我的项目中想要的是一个自包含的、自给自足的工具包,它可以在任何地方运行自己的测试,而不太依赖于它的操作环境。当您在 CI/CD 环境中操作时,这种可移植性变得更加重要。让我们看看所有这些是如何一起玩的。

用 jOOQ 进行单元测试

考虑下面的 jOOQ 查询方法:

public static void selectWithOptionalCondition(boolean hasFilter, Map<String, Object> filterValues) throws SQLException {
        try (Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/edens_car?user=root&password=admin")) {
            DSLContext context = DSL.using(connection, SQLDialect.MYSQL);
            Condition conditionChainStub = DSL.noCondition();
            if (hasFilter) {
                for(String key: filterValues.keySet()){
                    conditionChainStub = conditionChainStub.and(field(key).eq(filterValues.get(key)));
                }
            }
            List<CompleteVehicleRecord> allVehicles = context.select().from(table("complete_car_listing")).where(conditionChainStub).fetchInto(CompleteVehicleRecord.class);
            logger.info(allVehicles.toString());
        }
    }

我正在为 jOOQ 查询的WHERE子句做一些花哨的构造,动态构造将被翻译成该子句的Condition。除了最终需要发生的数据库查询之外,我如何验证我的条件链将产生我期望的WHERE子句?这就是单元测试的用武之地。

使用 Mockito

就像我之前提到的,Mockito 是一个非常流行的模仿框架,它允许你删除测试中不需要调用的代码部分。它还允许您用其他东西替换部分代码,以便于特定的测试场景。对于我的用例,我想验证我的查询中的条件链是否正常工作——我不需要查询结果。我首先将 Mockito 作为一个依赖项添加到我的项目中:

<dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <version>3.12.1</version>
        </dependency>
 <dependency>
           <groupId>org.mockito</groupId>
           <artifactId>mockito-junit-jupiter</artifactId>
           <version>3.12.1</version>
  </dependency>

这些 Maven 依赖项将为我的项目提供使用 Mockito 所需的库。这个Mockito-inline工件尤其重要,因为它提供了对模仿静态方法的支持。对该特性的需求很快就会变得明显。mockito-junit-jupiter工件是为最新版本的 JUnit 规定的;对于旧版本的 JUnit,请使用mockito-core来代替。继续编码!

@ExtendWith(MockitoExtension.class) (1)
@TestInstance(TestInstance.Lifecycle.PER_CLASS) (2)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) (3)
class JooqUnitTests {
      static MockedStatic mockedDriver; (4)
      final Logger logger = LoggerFactory.getLogger(JooqUnitTests.class);

      @BeforeAll (5)
      public static void prepare(){
             mockedDriver = mockStatic(DriverManager.class); (5b)
      }
 //more to come
}

前面的代码片段演示了我不久将需要的一些测试装置的设置:

  1. @ExtendWith是一个 JUnit 组件,允许用户使用自定义代码插入运行时。然后,不同的供应商可以提供一个类来完成契约并在这里可用。在本例中,我使用的是 Mockito 的MockitoExtension类。将 Mockito 的特性引入到这个测试单元中。

  2. @TestInstance是一个 JUnit 组件,配置测试类的生命周期。在Lifecycle.PER_CLASS中,我指定了我希望JooqUnitTests的一个实例可以被类中任意数量的测试方法重用。这样,测试方法可以在多次调用中共享状态。

  3. 确定测试用例将如何在报告、IDE 和其他地方显示。使用ReplaceUnderscores,我可以在我的测试方法名称中使用下划线,在显示时它们将被空格替换。这样,方法名可以是用户友好的句子,甚至非工程师(例如,产品所有者)也可以理解和使用。

  4. 是另一个 Mockito 测试夹具,它允许我模仿静态方法和接口。我将使用它来拦截来自 JDBC 的DriverManager.getConnection交互。

  5. @BeforeAll规定在运行任何测试方法之前,带注释的方法prepare要运行一次

    1. 这样我就可以定制DriverManager类的行为来满足我的需要

准备工作完成后,让我们继续进行单元测试。抓紧你的座位,太多了:

@ParameterizedTest (1)
@CsvSource({                (1a)
      "BLUE,2020",
      "SILVER,2020"
})
void test_dynamic_condition_api(String color,String year) throws SQLException {
      MockDataProvider mockJooqProvider = context -> { (2)
                        MockResult[] results = new MockResult[1];
                        String sql = ctx.sql();
                        logger.info(()->"Binding 1: "+ctx.bindings()[0]);
                        assertAll(()->{
                              assertTrue(ctx.bindings().length == 2 ); // validate two parameters are bound;
                              assertEquals(ctx.bindings()[0],color);
                              assertEquals(ctx.bindings()[1],year);
                        });
                        CompleteCarListingRecord completeCarListing = new CompleteCarListingRecord();
                        results[0] = new MockResult(completeCarListing);
                        return new MockResult[0];
                  }
            };
            MockConnection mockConnection = new MockConnection(mockJooqProvider); (3)
            mockedDriver.when(()-> DriverManager.getConnection(anyString())).thenReturn(mockConnection); (4)
      JooqApplication.selectWithOptionalCondition(true,Map.of("color",color,"year",year));
      }

该测试的主要目标是确保过滤器参数得到正确处理。作为第二个目标,我不想也不需要对数据库执行实际的查询。所以我需要用 JDBC 的用法替换其他的。这就是 jOOQ 的MockConnectionMockDataProvider的用武之地:

  1. JUnit 提供了@ParameterizedTest,允许我们从多个来源将数据输入到测试方法中。

    1. 这里,我使用@CsvSource选项来模拟 CSV 数据被传入。对于我提供的每一行,JUnit 将解析列,并将它们作为方法参数提供给测试方法。
  2. 为了从 jOOQ 提供一个MockConnection来替换合法的 JDBC 连接,我需要构建一个MockDataProvider

    1. 在我的MockDataProvider实现中,我可以访问一些非常好的测试装置,比如MockExecutionContext,将要执行的 SQL,以及关键的:提供给查询的参数绑定。然后,我会对它们进行验证,以确保它们存在并且计数正确。这里有很大的灵活性,允许许多测试用例。

    2. MockDataProvider#execute的契约要求我返回一个MockResult的数组。因为我并不关心这个场景中的结果,我只是从一个生成的类中构造了一个空的Record,然后继续。

  3. 实现了我的MockDataProvider,我可以继续构建一个MockConnection

  4. 还记得之前我用MockedStatic模仿DriverManager的时候吗?现在是时候闪耀了!去掉了DriverManager,我可以规定当任何字符串被传递给getConnection方法时,我的MockConnection应该被返回,而不是一个实际的 JDBC 连接。

有了所有这些设置,我就可以执行我的业务逻辑,看看事情是如何发展的。将不检索任何数据;这都与那一种方法有关。

使用 SQL 解析

jOOQ 附带了一些不一定与 SQL 执行有任何关系的 SQL 解析功能。您可以使用Parser类从纯文本 SQL 生成 jOOQ 组件;在这个过程中,它会让你知道你的 SQL 是否合法。观察:

@Test
void validate_my_dodgy_sql(){
      assertThrows(ParserException.class, ()->
                  DSL.using(SQLDialect.MYSQL)
                         .parser()
                         .parse("selecet * from table group by 1 where having max (column) > 10"));
}

那个 SQL 不对, 6 我相信你会同意的。通过 JUnit 的assertThrows,我已经指定我期望这个尝试parse明文 SQL 应该失败,并出现一个ParserExceptionVia con Dios!

img/500754_1_En_5_Figc_HTML.png

yawwwn

相信我,伙计们:BDD 就是它所在的地方。当您处理数据时,您真的想亲自动手执行 SQL 语句;看到真实的结果。你明白我的意思吗?希望如此。因为我们就要开始有趣的部分了!

用 Docker 和 TestContainers 进行集成测试

像我前几页提到的,Docker 是容器的运行时。如果你不熟悉这个概念,可以把 Docker 想象成一个虚拟机——如果你愿意的话,可以称之为 JVM。就像你可以下载任何一种由第三方打包的 JAR,并在你的 JVM 中运行一样,Docker 也有类似的功能。不同厂商发布图片到 Docker Hub 7 然后你可以拉下这些映像并运行基于映像的容器。从某种意义上说,Docker Hub 是容器世界的专家中心。你可以得到几乎任何一个主要的软件作为一个映像,因此,一个容器。这为您提供了极大的灵活性和可移植性,使您能够以一种可移植的、几乎是轻量级的格式运行以前庞大且开销大的软件,这样您就可以以一种动态和灵活的形式运行整个操作系统、CI/CD 服务器和工具、关键的基础设施软件,当然还有数据库。

TestContainers 是一个 Java 库,帮助您进一步提高容器的可移植性。它给了你从 Java 代码中运行任何容器化软件的能力。

img/500754_1_En_5_Figd_HTML.jpg

www.testcontainers.org

作为 JUnit 测试的一部分,我们现在将使用 TestContainers 启动一个 MySQL 数据库服务器,并加载真实的表和数据。然后,我们可以针对它运行实际的代码——这里没有嘲笑的事情。嗯,也许只是一点点。我们走吧!

首先,为你的操作系统下载/安装 Docker—www.docker.com对于大多数用户来说是一个很好的起点。TestContainers 依赖 Docker 运行时来施展魔法。没有 JVM 就不能运行 JAR 文件,是吗?

和往常一样,Maven 依赖项排在第一位:

<dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers</artifactId>
            <version>1.16.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>1.16.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>mysql</artifactId>
            <version>1.16.0</version>
            <scope>test</scope>
        </dependency>

和莫奇托一样,请注意神器。对于它支持的每个数据库版本(有很多),TestContainers 都有一个专用的 Maven 依赖项。为了使用 MySQL,我添加了mysql工件;为您选择的数据库容器做出正确选择。

现在,为我的下一个演示,做一点测试准备:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@Testcontainers (1)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class JooqIntegrationTests {

    static MockedStatic mockedDriver;
    final static String DATABASE_NAME = "edens_car";
    final static String USERNAME = "auserhasnoname";
    final static String PW = "anawfulpassword";

    @Container (2)
    static GenericContainer mySqlContainer = new MySQLContainer(DockerImageName.parse("mysql:latest")) (3)
            .withDatabaseName(DATABASE_NAME)
            .withInitScript("schema_with_data.sql")
            .withUsername(USERNAME)
            .withPassword(PW)
            .withEnv("TESTCONTAINERS_CHECKS_DISABLE","true")
            .withExposedPorts(3306);

   @BeforeAll
   public static void setup(){
      mockedDriver = mockStatic(DriverManager.class); (4)
   }
}

请记住,这只是在测试课中的准备工作;实际的测试方法将很快出现:

  1. @TestContainers是 TestContainers 库提供的 JUnit 扩展。它实际上是我们之前在 Mockito 中看到的ExtendWith注释的一个外观。

  2. @Container也由 TestContainers 提供。有了这个注释,TestContainers 可以挂钩到 JUnit 运行时的生命周期中,并提前准备容器实例。

  3. GenericContainer类是一个……通用类,它包装了大多数 TestContainers 的基于容器的功能。

    1. 就像我们对 Maven 和任何其他依赖管理系统所做的一样,我必须为 hub 中适当的 Docker 映像提供命名坐标。格式为“名称”:“标签”。这里,我说我想要带有“最新”标签或版本的“mysql”图像。

    2. 允许我为我尚未创建的数据库设置一个名称。

    3. withInitScript定义 SQL 脚本文件的名称,该文件将在容器完成初始化后立即加载。这允许我在任何实际的测试执行之前用 DDL 和 DML 填充我的数据库。

    4. 我用凭证信息withUsernamewithPassword播种数据库容器。

    5. 我还可以用withEnv将随机环境变量传递到我的容器中。这里,我提供了一个 TestContainers 命令行参数,允许它跳过一些启动检查,从而更快地准备好数据库容器。

    6. 最后,我定义了 MySQL 应该监听的端口。请注意,这仍然在容器内部。TestContainers 将为我发布一个单独的随机端口,以便我能够连接到 MySQL 容器。这个过程在容器世界中被称为端口映射。

  4. 最后,和我们之前做的一样,我准备模拟出DriverManager,因为我想提供一个动态生成的Connection——但这次是一个真实的 MySQL 数据库。

现在我已经设置好了所有的测试装置,我可以继续编写实际的测试了:

@Test
 public void test_containerized _connection() throws SQLException {
        JdbcDatabaseContainer container = (JdbcDatabaseContainer) mySqlContainer; (1)
        Connection connection =container.createConnection("");    (2)
        mockedDriver.when(()-> DriverManager.getConnection(anyString())).thenReturn(connection); (3)
        JooqDemoApplication.insertVehicle();
    }

在前面的代码片段中

  1. 我将GenericContainer转换成一种更特殊的形式,即JdbcDatabaseContainer

  2. 这现在允许我直接从容器中获得一个 JDBC Connection的实例。

  3. 然后我可以把我自己的Connection代入DriverManager

接下来,我可以直接执行我的测试逻辑代码。

这是一个正确的测试。它包含实际的数据、实际的数据库设备和修整,所有这些都在一个真实的数据库中。令人愉快。

这是 JUnit 允许的另一件漂亮的事情:测试方法排序。通过测试方法排序,您可以拥有相互依赖的测试,或者至少必须以特定的顺序运行。看看吧:

   @Test
   @Order(1)
   public void test_containerized_connection() throws SQLException {
       ...
    }

@Test
@Order(2)
public void test_valid_db_insert() throws SQLException {
      if(!mySqlContainer.isRunning()){
            mySqlContainer.start();
        }
        JdbcDatabaseContainer container = (JdbcDatabaseContainer) mySqlContainer;
        container.getJdbcUrl();
        Connection connection =container.createConnection("");
        DSLContext context = DSL.using(connection, SQLDialect.MYSQL);
        List<CompleteVehicleRecord> allVehicles = context.select(field(name("brand")), field("model"), field("price")).from(table("complete_car_listing")).orderBy(field("year").asc(), two()).fetchInto(CompleteVehicleRecord.class);
        assertTrue(allVehicles.size() == 1);
   }

@Order注释允许我规定test_valid_db_insert应该在test_containerized_connection之后立即执行。这就是事情变得有点不稳定的地方。

看,TestContainers 在测试方法执行完毕后会立即关闭容器。容器实际上没有被销毁,但是它没有运行。这就是为什么在跨测试方法重用容器实例时有必要采取一些预防措施。在这个场景中,我用test_containerized_connection插入了数据;然后我想验证test_valid_db_insert中的插入。我必须检查集装箱是否还在isRunning上;否则,测试失败。如果容器没有运行,我可以用start重启它。这是一种支持容器重用的非常粗糙的机制;有了它,你可以变得更加漂亮和易于维护。

Pro Tip

TestContainers 提供了ScriptUtils.runInitScript实用程序,帮助针对数据库容器执行任意 SQL 脚本。这样,即使在数据库的初始加载之后,您也可以在测试的任何时候执行定制的 SQL。

从“遗产很可爱”的角度来看,这一切都很好。如果你在代码中使用容器,你可能不会处理DriverManager。你可能是一个框架型的人。我们试试这件怎么样…

和 Spring Boot 一起

Spring Boot 是,嗯,Spring Boot。它提供了一整套令人眼花缭乱的测试装置和组件。我们不会深究所有这些。我们来这里只是为了快乐。看看吧:

@SpringBootTest (1)
@Testcontainers
public class JooqSpringBootTests {

    @Autowired
    JooqBean jooqBean; (2)
     ...
    @Container
    static GenericContainer mySqlContainer = new MySQLContainer(DockerImageName.parse("mysql:latest"))
    ...
    @DynamicPropertySource (3)
    static void postgresqlProperties(DynamicPropertyRegistry registry) {
        JdbcDatabaseContainer container = (JdbcDatabaseContainer) mySqlContainer;
        registry.add("spring.datasource.url", container::getJdbcUrl);
        registry.add("spring.datasource.password", container::getPassword);
        registry.add("spring.datasource.username", container::getUsername);
    }

    @Test
    @Sql("/schema_with_data.sql") (3)
    public void test_springboot_loading(){
        List<Vehicle> vehicles = jooqBean.runSql();
        assertTrue(vehicles.size() >= 1);
    }
}

我已经精简了这段代码,排除了你到目前为止看到的旧内容。我们来这里是为了新的,仅仅是新的:

  1. 有了@SpringBootTest,Spring 将会注意到并提供它的设施。

  2. 这就是我现在如何注入包含各种 jOOQ 查询的JooqBean

  3. Spring Boot 在 2.2.6 版中新增了@DynamicPropertySource注释,它允许我动态地覆盖我选择的任何框架属性。这在动态构建未知端口、用户名和密码的数据库容器时特别有用。

  4. 最后,在测试方法本身上,我部署了同样来自 Spring 的@Sql组件。该注释将执行所提供的脚本文件中的 SQL 语句。默认行为是在测试方法运行之前执行脚本*,但是这是可以改变的。此外,我可以为不同的目的提供任意数量的脚本。相当整洁。*

现在,您已经对 TestContainers 有所了解,让我们重新审视一下我们的打包困境:我们如何在不需要外部数据库服务器的情况下,将更改应用到我们的模式,生成更新的 jOOQ 类,以及运行我们的测试?我已经演示了为支持这个目标需要做的一些准备工作。现在让我们看看支持它的代码。

public static GenericContainer startDatabaseContainer() throws SQLException {
         mySql = new MySQLContainer(DockerImageName.parse("mysql:latest"))
                .withDatabaseName(DATABASE_NAME)
                .withUsername(USERNAME)
                .withPassword(PW)
                .withEnv("TESTCONTAINERS_CHECKS_DISABLE","true")
                .withExposedPorts(3306);
        mySql.start();
        return container;
    }

前面的代码片段与我在测试业务中展示的代码没有太大的不同。这里的主要区别是我用start方法显式地启动数据库容器。是的,还有一个stop方法可以在你完成时使用。启动容器化的 MySQL 后,我可以用 Flyway 执行我的迁移。

//run the migration with a connection to the database container
public static void runMigrations(GenericContainer container){
        JdbcDatabaseContainer container = (JdbcDatabaseContainer) container;
        Flyway flyway = Flyway.configure().dataSource(container.getJdbcUrl(),container.getUsername(),container.getPassword()).load();
        flyway.migrate();
    }

然后把所有的东西绑在一起:

public static void main(String[] args) throws SQLException {
            logger.info("Running preflight operations");
            GenericContainer mySql = startDatabaseContainer();
            runMigrations(mySql);
            generateJooqCode(mySql);
            connection.close();
            mySql.close();
    }

因此,我们可以拥有一个完全自给自足的项目,至少从数据库的角度来看是这样的。这可以在开发机器或构建服务器上运行。

总的来说,您需要一个自包含、自持续的软件项目工具包,它可以

  1. 可移植性随着数据库模式的变化而发展

  2. 在任何地方运行它的测试——在开发人员的机器上,在构建管道中,在拉请求合并之前,等等。

  3. 在没有相关开销的情况下,针对类似生产的软件和基础架构验证您的假设

  4. 向您保证数据库相关代码的语法正确性

因为毕竟这是现代软件开发的全部内容。

祝你好运,感谢你的阅读!

Footnotes 1

https://en.wikipedia.org/wiki/Evolutionary_database_design

  2

www.agilealliance.org/glossary/bdd/

  3

https://stackoverflow.com/a/153565/1530938

  4

编者按:热拿!

  5

我特别喜欢没有公开文档的强大的QuarkusUnitTest类(但它在我的书中)。强烈推荐用于集成测试。

  6

是的,我是一个山中之王书呆子。你也应该这样。

  7

https://hub.docker.com/