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>

        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>build-helper-maven-plugin</artifactId>
            <executions>
                <execution>
                    <phase>generate-sources</phase>
                    <goals>
                        <goal>add-source</goal>
                    </goals>
                    <configuration>
                        <sources>
                            <source>src/main/jooq</source>
                        </sources>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</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:入门