51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring Boot + jOOQ 教程 - 1:入门

jOOQ 是一个 Java 持久库,提供用于编写类型安全 SQL 查询的 SQL DSL 。它支持大多数流行的数据库,如 MySQLPostgreSQLOracleSQL Server 等。

本文将带你了解如何在 Spring Boot 中使用 jOOQ 实现持久层。JOOQ 也可以在 KotlinScala 等其他基于 JVM 的语言中使用。

本系列教程中,将带你学习如何在 Spring Boot 中使用 JOOQ 实现:

  • 基本的 CRUD 操作
  • 一对一关系检索
  • 一对多关系检索
  • 多对多关系检索

你可以在 Github 获取完整的源码。

前提条件 {#前提条件}

  • 安装 JDK 17 或更高版本
  • 安装任何容器运行时,如 Docker DesktopOrbStack 等。

注意 :jOOQ 不需要 Docker。只是使用 Testcontainers 进行 jOOQ 代码生成和测试需要一个容器运行时。

示例数据库 {#示例数据库}

本教程使用下列示例数据库。

示例数据库表结构

创建 Spring Boot 应用 {#创建-spring-boot-应用}

使用 Spring Initializr 创建一个 Spring Boot 项目。选择 JOOQ Access LayerFlyway MigrationPostgreSQL DriverTestcontainers

你可以点击该 链接,快速创建此项目。

添加 Flyway 迁移脚本 {#添加-flyway-迁移脚本}

使用 Flyway 进行数据库 schema 迁移。

src/main/resources/db/migration 目录下添加以下 SQL 脚本。

V1__create_tables.sql

CREATE TABLE user_preferences
(
    id         bigserial primary key,
    theme      varchar(255),
    language   varchar(255),
    created_at timestamp with time zone default CURRENT_TIMESTAMP,
    updated_at timestamp with time zone
);

CREATE TABLE users ( id bigserial primary key, name varchar(255) not null, email varchar(255) not null, password varchar(255) not null, preferences_id bigint REFERENCES user_preferences (id), created_at timestamp with time zone default CURRENT_TIMESTAMP, updated_at timestamp with time zone, CONSTRAINT user_email_unique UNIQUE (email) );

CREATE TABLE bookmarks ( id bigserial primary key, url varchar(1024) not null, title varchar(1024), created_by bigint not null REFERENCES users (id), created_at timestamp with time zone default CURRENT_TIMESTAMP, updated_at timestamp with time zone );

CREATE TABLE tags ( id bigserial primary key, name varchar(100) not null, created_at timestamp with time zone default CURRENT_TIMESTAMP, updated_at timestamp with time zone, CONSTRAINT tag_name_unique UNIQUE (name) );

CREATE TABLE bookmark_tag ( bookmark_id bigint not null REFERENCES bookmarks (id), tag_id bigint not null REFERENCES tags (id) );

ALTER SEQUENCE user_preferences_id_seq RESTART WITH 101; ALTER SEQUENCE users_id_seq RESTART WITH 101; ALTER SEQUENCE bookmarks_id_seq RESTART WITH 101; ALTER SEQUENCE tags_id_seq RESTART WITH 101;

SQL 脚本中更改了序列,使其从 101 开始,这样就可以插入一些 ID 为 123 等的测试数据。

使用 jOOQ 执行本地 SQL 查询 {#使用-jooq-执行本地-sql-查询}

添加 jOOQ Starter 依赖后,Spring Boot 会将 jOOQ 的 DSLContext 自动配置为一个 Bean。

可以使用 DSLContext Bean 执行本地 SQL 查询。

创建 UserRepository 类,如下:

package com.sivalabs.bookmarks.repositories;

import org.jooq.DSLContext; import org.jooq.Record; import org.springframework.stereotype.Repository;

@Repository class UserRepository { private final DSLContext dsl;

UserRepository(DSLContext dsl) {
    this.dsl = dsl;
}

public String findUserNameById(Long id) {
    Record record =
            dsl.resultQuery("select * from users where id = ?", id)
               .fetchOptional().orElseThrow();
    System.out.println(record);
    Object name = record.get("name");
    return (String) name;
}

}

在编写测试之前,创建一个 SQL 脚本,将一些测试数据添加到数据库中。

src/test/resources/test-data.sql

DELETE FROM bookmark_tag;
DELETE FROM bookmarks;
DELETE FROM tags;
DELETE FROM users;
DELETE FROM user_preferences;

INSERT INTO user_preferences (id, theme, language) VALUES (1, 'Light', 'EN'), (2, 'Dark', 'EN') ;

INSERT INTO users (id, email, password, name, preferences_id) VALUES (1, 'admin@gmail.com', 'admin', 'Admin', 2), (2, 'siva@gmail.com', 'siva', 'Siva', 1) ;

INSERT INTO tags(id, name) VALUES (1, 'java'), (2, 'spring-boot'), (3, 'spring-cloud'), (4, 'devops'), (5, 'security') ;

INSERT INTO bookmarks(id, title, url, created_by, created_at) VALUES (1, 'SivaLabs', 'https://sivalabs.in', 1, CURRENT_TIMESTAMP), (2, 'Spring Initializr', 'https://start.spring.io', 2, CURRENT_TIMESTAMP), (3, 'Spring Blog', 'https://spring.io/blog', 2, CURRENT_TIMESTAMP) ;

insert into bookmark_tag(bookmark_id, tag_id) VALUES (1, 1), (1, 2), (1, 3), (2, 2) ;

创建一个简单的测试用例来验证上述代码。

package com.sivalabs.bookmarks.repositories;

import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jooq.JooqTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.annotation.Import; import org.springframework.test.context.jdbc.Sql; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers;

import static org.assertj.core.api.Assertions.assertThat;

@JooqTest @Import({UserRepository.class}) @Testcontainers @Sql("classpath:/test-data.sql") class UserRepositoryTest {

@Container
@ServiceConnection
static final PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");

@Autowired
UserRepository userRepository;

@Test
void findUserNameById() {
    String username = userRepository.findUserNameById(1L);
    assertThat(username).isEqualTo("Admin");
}

}

使用 @JooqTest 测试注解来测试 Repository 类。使用 @Testcontainers@Container 注解启动 PostgreSQL 数据库,并使用 @ServiceConnection 注册 DataSource properties。此外,还使用 @Sql 注解执行 test-data.sql 脚本。

在测试方法中,调用 userRepository.findUserNameById(1L) 并验证结果。

开启 JOOQ 的 SQL 日志

application.properties 文件中添加以下属性,即可开启 JOOQ 的 SQL 日志:

logging.level.org.jooq.tools.LoggerListener=DEBUG

开启了 JOOQ 的 SQL 日志后,可以在控制台中看到格式化后的输出:

+----+-----+---------------+--------+--------------+--------------------------------+----------+
|  id|name |email          |password|preferences_id|created_at                      |updated_at|
+----+-----+---------------+--------+--------------> +--------------------------------+----------+
|   1|Admin|admin@gmail.com|admin   |             1|2023-10-12T11:01:58.471277+05:30|{null}    |
+----+-----+---------------+--------+--------------+--------------------------------+----------+

如上,测试 OK。

在当前的实现中,如果将某个字符串作为 id 值传递,也不会有任何编译错误。而且,以 Object 类型获取 name 值后,需要将其强制转换为所需的 String 类型。

为此,可以使用 jOOQ 的 Typesafe DSL (类型安全的 DSL)。这首先需要从数据库 schema 生成 jOOQ 类。

jOOQ 代码生成 {#jooq-代码生成}

使用 jOOQ 代码生成工具,根据数据库 schema 生成 jOOQ 类。

要使用 jOOQ 代码生成工具,需要有一个已运行的数据库。

可以使用 Testcontainers 启动 PostgreSQL 数据库容器,然后使用 jOOQ 代码生成工具生成 jOOQ 类。

有一个 testcontainers-jooq-codegen-maven-plugin Maven 插件可以启动数据库容器,运行 Flyway 迁移,然后生成 jOOQ 代码。

pom.xml 文件中配置 testcontainers-jooq-codegen-maven-plugin,如下:

<properties>
    <testcontainers.version>1.19.1</testcontainers.version>
    <tc-jooq-codegen-plugin.version>0.0.3</tc-jooq-codegen-plugin.version>
</properties>

<build> <plugins> <plugin> <groupId>org.testcontainers</groupId> <artifactId>testcontainers-jooq-codegen-maven-plugin</artifactId> <version>${tc-jooq-codegen-plugin.version}</version> <dependencies> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <version>${testcontainers.version}</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>${postgresql.version}</version> </dependency> </dependencies> <executions> <execution> <id>generate-jooq-sources</id> <goals> <goal>generate</goal> </goals> <phase>generate-sources</phase> <configuration> <database> <type>POSTGRES</type> <containerImage>postgres:16-alpine</containerImage> </database> <flyway> <locations> filesystem:${project.basedir}/src/main/resources/db/migration </locations> </flyway> <jooq> <generator> <generate> <javaTimeTypes>true</javaTimeTypes> </generate> <database> <inputSchema>public</inputSchema> <includes>.*</includes> <excludes> flyway_schema_history </excludes> </database> <target> <clean>true</clean> <packageName>com.sivalabs.bookmarks.jooq</packageName> <directory>src/main/jooq</directory> </target> </generator> </jooq> </configuration> </execution> </executions> </plugin>

    &lt;plugin&gt;
        &lt;groupId&gt;org.codehaus.mojo&lt;/groupId&gt;
        &lt;artifactId&gt;build-helper-maven-plugin&lt;/artifactId&gt;
        &lt;executions&gt;
            &lt;execution&gt;
                &lt;phase&gt;generate-sources&lt;/phase&gt;
                &lt;goals&gt;
                    &lt;goal&gt;add-source&lt;/goal&gt;
                &lt;/goals&gt;
                &lt;configuration&gt;
                    &lt;sources&gt;
                        &lt;source&gt;src/main/jooq&lt;/source&gt;
                    &lt;/sources&gt;
                &lt;/configuration&gt;
            &lt;/execution&gt;
        &lt;/executions&gt;
    &lt;/plugin&gt;
&lt;/plugins&gt;

</build>

上述代码配置了 testcontainers-jooq-codegen-maven-pluginsrc/main/jooq 目录中生成 jOOQ 代码。然后,使用 build-helper-maven-pluginsrc/main/jooq 目录添加为源码目录。

将生成的 jOOQ 代码添加到源码?

你也可以在 target/generated-sources/jooq 目录中生成代码,Maven 会自动将其添加为源码。这样就不需要使用 build-helper-maven-plugin 了。

不过,我个人更喜欢在 src/main/jooq 目录中生成代码,并将生成的代码纳入源码目录。

现在,运行以下命令来生成 jOOQ 代码。

$ ./mvnw clean generate-sources

使用 JOOQ DSL {#使用-jooq-dsl}

现在,重写 UserRepository 类,使用 jOOQ DSL。

package com.sivalabs.bookmarks.repositories;

import com.sivalabs.bookmarks.jooq.tables.records.UsersRecord; import org.jooq.DSLContext; import org.springframework.stereotype.Repository;

import static com.sivalabs.bookmarks.jooq.tables.Users.USERS;

@Repository class UserRepository { private final DSLContext dsl;

UserRepository(DSLContext dsl) {
    this.dsl = dsl;
}

public String findUserNameById(Long id) {
    UsersRecord usersRecord = dsl.selectFrom(USERS)
            .where(USERS.ID.eq(id))
            .fetchOptional().orElseThrow();
    return usersRecord.getName();
}

}

如你所见,使用的是 jOOQ 代码生成工具生成的 UsersRecord 类。现在,如果为 id 参数传递除 Long 以外的其他类型,就会出现编译错误。此外,获取的 name 值是就是 String 类型。

因此,jOOQ Typesafe DSL 对于编写类型安全的 SQL 查询非常有用。

总结 {#总结}

本文介绍了如何在 Spring Boot 中整合 jOOQ 以及如何配置其代码生成器。


Ref:https://www.sivalabs.in/spring-boot-jooq-tutorial-getting-started/

赞(2)
未经允许不得转载:工具盒子 » Spring Boot + jOOQ 教程 - 1:入门