跳到主要内容

6. Micronaut Data JDBC 和 R2DBC

Micronaut Data JDBC / R2DBC 是一个预计算本地 SQL 查询(给定一个特定的数据库方言)并提供一个仓库的实现,它是本地结果集和实体之间的一个简单数据映射器。

Micronaut Data JDBC / R2DBC 支持 Micronaut Data for JPA 的所有功能,包括动态查找器分页投影数据传输对象(DTO)批量更新优化锁定等。

但是,Micronaut Data JDBC / R2DBC 不是对象关系映射(ORM)实现,现在和将来都不会包含以下任何概念:

  • 关联的懒加载或代理
  • 脏检查
  • 持久化上下文/会话
  • 一级缓存和实体代理

Micronaut Data JDBC / R2DBC 是为喜欢低级体验和直接使用 SQL 的用户设计的。

注意

Micronaut Data JDBC / R2DBC 可用于实现典型应用程序中存在的大多数简单 SQL 查询,并且不包括任何运行时查询构建 DSL。对于更复杂的查询,Micronaut Data JDBC / R2DBC 可以与现有的许多优秀的 Java SQL DSL(如 JOOQQueryDSLRequery 或甚至 JPA)之一搭配使用。

6.1 JDBC

Micronaut Data JDBC 是为喜欢低级体验和直接使用 SQL 的用户设计的。 以下部分包含 JDBC 的具体配置和文档。

6.1.1 快速入门

最快速的入门方法是使用 Micronaut Launch 创建一个新的 Micronaut 应用程序,并选择 data-jdbc、数据库驱动和数据库迁移框架功能。这也可以通过 CLI 完成。

提示

您还可以在 Micronaut 指南中找到关于构建 Micronaut 数据 JDBC 应用程序的精彩指南,包括各种语言的示例代码:使用 Micronaut Data JDBC 访问数据库

点击下表中的一个链接,您将进入 Micronaut Launch,其中的相应选项已根据您选择的语言和构建工具进行了预配置:

表 1. 使用 Micronaut Launch 创建 JDBC 应用程序

GradleMaven
Java打开打开
Kotlin打开打开
Groovy打开打开

使用 CLI 创建应用程序

# For Maven add: --build maven
$ mn create-app --lang java example --features data-jdbc,flyway,mysql,jdbc-hikari

或通过 curl

使用 curl 创建应用程序

# For Maven add to the URL: &build=maven
$ curl https://launch.micronaut.io/demo.zip?lang=java&features=data-jdbc,flyway,mysql,jdbc-hikari -o demo.zip && unzip demo.zip -d demo && cd demo

生成的应用程序将在编译范围内依赖于 micronaut-data-jdbc 模块,并将使用 MySQL,因为我们通过 mysql 特性添加了对 MySQL JDBC 驱动的依赖:

implementation("io.micronaut.data:micronaut-data-jdbc")

还应确保已配置 JDBC 驱动和连接池依赖关系:

runtimeOnly("io.micronaut.sql:micronaut-jdbc-hikari")

注解处理器需要正确设置 Micronaut 数据处理器依赖关系,以实现编译时生成和评估:

annotationProcessor("io.micronaut.data:micronaut-data-processor")
注意

对于 Kotlin,依赖应位于 kapt 范围中;对于 Groovy,依赖应位于 compileOnly 范围中。

接下来,你需要配置至少一个数据源。应用程序配置文件中的以下代码段是配置默认 JDBC 数据源的示例:

datasources.default.url=jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE;NON_KEYWORDS=USER
datasources.default.driverClassName=org.h2.Driver
datasources.default.username=sa
datasources.default.password=
datasources.default.schema-generate=CREATE_DROP
datasources.default.dialect=H2
注意

schema-generate 设置仅对演示和测试微不足道的示例有用,对于生产使用,建议将 Micronaut Data 与 SQL 迁移工具,如 FlywayLiquibase 搭配使用。

要从数据库中检索对象,你需要定义一个 @MappedEntity 注解的类。请注意,这是一个元注解,实际上,如果你愿意,可以使用 JPA 注释(仅支持一部分,稍后详述)。如果您希望使用 JPA 注释,请包含以下仅 compileOnly 范围的依赖:

compileOnly("jakarta.persistence:jakarta.persistence-api")

要在 javax.persistence 包中使用 JPA 注释,请使用:

compileOnly("jakarta.persistence:jakarta.persistence-api:3.0.0")
警告

如果您想在使用 Micronaut Data JDBC 的实体中使用 JPA 注释,我们强烈建议您使用 jakarta.persistence 注释。Micronaut Data 将在未来移除对 javax.persistence 注解的支持。

如上所述,由于只使用注解,因此依赖关系只能在编译时包含,而不能在运行时包含,这样就不会拖累其他 API,从而减少 JAR 文件的大小。 然后,您可以定义一个 @Entity

package example;

import jakarta.persistence.*;

@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private int pages;

public Book(String title, int pages) {
this.title = title;
this.pages = pages;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getTitle() {
return title;
}

public int getPages() {
return pages;
}
}

随后是一个从 CrudRepository 扩展而来的接口。

package example;

import io.micronaut.data.annotation.*;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.*;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;
import java.util.List;


@JdbcRepository(dialect = Dialect.H2) // (1)
interface BookRepository extends CrudRepository<Book, Long> { // (2)
Book find(String title);
}
  1. 该接口使用 @JdbcRepository 进行注解,并指定了用于生成查询的 H2 方言
  2. CrudRepository 接口接受 2 个通用参数,即实体类型(本例中为 Book)和 ID 类型(本例中为 Long)。

现在,您可以对实体执行 CRUD(创建、读取、更新、删除)操作。example.BookRepository 的实现是在编译时创建的。要获得对它的引用,只需注入 Bean:

@Inject BookRepository bookRepository;

保存实例(创建)

要保存实例,请使用 CrudRepository 接口的保存方法:

Book book = new Book("The Stand", 1000);
bookRepository.save(book);
注意

与 JPA 实现不同的是,没有脏检查,因此 save 总是执行 SQL INSERT。对于批量更新,请使用 update 方法(参见下一节)。

检索实例(读取)

要回读一个 book,请使用 findById 方法:

book = bookRepository.findById(id).orElse(null);

更新实例(更新)

使用 Micronaut Data JDBC,您必须手动实现 update 方法,因为 JDBC 实现不包括任何脏检查或持久化会话概念。因此,您必须为仓库中的更新定义明确的更新方法。例如:

void update(@Id Long id, int pages);

void update(@Id Long id, String title);

然后就可以这样调用了:

bookRepository.update(book.getId(), "Changed");

删除实例(删除)

要删除一个实例,请使用 deleteById

bookRepository.deleteById(id);

恭喜您已经实施了第一个 Micronaut Data JDBC 仓库!继续阅读,了解更多信息。

6.1.2 配置

JDBC 驱动

Micronaut Data JDBC 要求配置一个适当的 java.sql.DataSource bean。

你可以手动完成或使用 Micronaut JDBC 模块,该模块提供开箱即用的支持,可配置 Tomcat JDBC、Hikari、Commons DBCP 或 Oracle UCP 的连接池。

SQL 日志

你可以通过为 io.micronaut.data.query 日志器启用跟踪日志来启用 SQL 日志。例如在 logback.xml 中:

启用 SQL 查询记录

<logger name="io.micronaut.data.query" level="trace" />

创建模式

要创建数据库模式,建议将 Micronaut Data 与 SQL 迁移工具,如 FlywayLiquibase 配对使用。

SQL 迁移工具可为在一系列数据库中创建和演化模式提供更全面的支持。

如果你想快速测试 Micronaut Data,那么你可以将数据源的 schema-generate 选项设置为 create-drop 以及适当的模式名称:

注意

大多数数据库迁移工具都使用 JDBC 驱动程序来更改数据库。如果使用 R2DBC,则需要单独配置 JDBC 数据源。

使用 schema-generate

datasources.default.url=jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE;NON_KEYWORDS=USER
datasources.default.driverClassName=org.h2.Driver
datasources.default.username=sa
datasources.default.password=
datasources.default.schema-generate=CREATE_DROP
datasources.default.dialect=H2

schema-generate 选项目前只推荐用于简单的应用程序、测试和演示,并不能用于生产。配置中设置的方言是用于生成模式的方言。

设置方言

如上配置所示,您还应配置方言。虽然查询会在资源库中预先计算,但在某些情况下(如分页)仍需要指定方言。下表总结了支持的方言:

表 1. 支持的 JDBC / R2DBC 方言

方言描述
H2H2 数据库(通常用于内存测试)
MYSQLMySQL 5.5 或更高版本
POSTGRESPostgres 9.5 或更高版本
SQL_SERVERSQL Server 2012 或更高版本
ORACLEOracle 12c 或更高版本
危险

配置中的方言设置不能取代确保在版本库中设置正确方言的需要。如果配置中的方言是 H2,那么版本库中应该有 @JdbcRepository(dialect = Dialect.H2) / @R2dbcRepository(dialect = Dialect.H2)。因为版本库是在编译时计算的,所以当时并不知道配置值。

6.2 R2DBC

Micronaut Data R2DBC 是专为喜欢较低级体验和直接使用 SQL 并希望构建非阻塞、反应式应用程序的用户设计的。 以下部分包含 R2DBC 的具体配置和文档。

6.2.1 快速入门

最快速的入门方法是使用 Micronaut Launchdata-r2dbc(数据库驱动程序和数据库迁移框架功能)创建一个新的 Micronaut 应用程序。这也可以通过 CLI 完成。

点击下表中的一个链接,就能进入 Micronaut Launch,其中的相应选项已根据你选择的语言和构建工具进行了预配置:

表 1. 使用 Micronaut Launch 创建 R2DBC 应用程序

Gradle
Java打开打开
Kotlin打开打开
Groovy打开打开

使用 CLI 创建应用程序

# For Maven add: --build maven
$ mn create-app --lang java example --features data-r2dbc,flyway,mysql

或通过 curl

使用 curl 创建应用程序

# For Maven add to the URL: &build=maven
$ curl https://launch.micronaut.io/demo.zip?lang=java&features=data-r2dbc,flyway,mysql -o demo.zip && unzip demo.zip -d demo && cd demo

由于我们通过 mysql 特性添加了对 MySQL 的 R2DBC 驱动的依赖,因此生成的应用程序将使用 MySQL:

runtimeOnly("dev.miku:r2dbc-mysql")

而对于 flyway 来说,则是 JDBC 驱动:

runtimeOnly("mysql:mysql-connector-java")
提示

要为其他驱动创建配置,可以选择相应的功能:OraclePostgresSQLServerH2Mariadb

现在定义一个 SQL 脚本,在 src/main/resources/db/migration 中创建初始模式。例如:

示例 V1__create-schema.sql

CREATE TABLE book(id SERIAL NOT NULL PRIMARY KEY, title VARCHAR(255), pages INT, author_id BIGINT NOT NULL);
CREATE TABLE author(id SERIAL NOT NULL PRIMARY KEY, name VARCHAR(255));

现在,您可以使用 src/main/resources 下的应用程序配置文件,配置应用程序连接数据库:

flyway.datasources.default.enabled=true
datasources.default.url=jdbc:mysql://localhost:3306/mydatabase
r2dbc.datasources.default.url=r2dbc:mysql:///mydatabase
  • 启用设置可确保应用 Flyway 模式迁移。更多信息参阅 Micronaut Flyway
  • Flyway 配置需要一个 JDBC 数据源,datasources.defaul.url 可配置一个。更多信息,参阅数据源配置
  • r2dbc.datasources.default.url 用于配置默认的 R2DBC ConnectionFactory
提示

R2DBC ConnectionFactory 对象可通过依赖注入注入到代码中的任何位置。

现在定义一个 @MappedEntity,映射到模式中定义的 author 表:

package example;

import io.micronaut.data.annotation.*;
import io.micronaut.serde.annotation.Serdeable;

@Serdeable
@MappedEntity
public class Author {
@GeneratedValue
@Id
private Long id;
private final String name;

public Author(String name) {
this.name = name;
}

public String getName() {
return name;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}
}

还有一个从 ReactiveStreamsRepository 扩展而来的存储库接口,用于访问数据库:

package example;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.r2dbc.annotation.R2dbcRepository;
import io.micronaut.data.repository.reactive.ReactiveStreamsCrudRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import jakarta.validation.constraints.NotNull;

@R2dbcRepository(dialect = Dialect.POSTGRES) // (1)
public interface AuthorRepository extends ReactiveStreamsCrudRepository<Author, Long> {
@NonNull
@Override
Mono<Author> findById(@NonNull @NotNull Long aLong); // (2)

@NonNull
@Override
Flux<Author> findAll();
}
  1. @R2dbcRepository 注解可用于指定数据源和方言
  2. 您可以覆盖超级接口中的方法,用具体实现来专门化默认 Publisher 返回类型

现在,您可以将此接口注入控制器,并用它来执行 R2DBC 查询:

package example;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Controller("/authors")
public class AuthorController {
private final AuthorRepository repository;

public AuthorController(AuthorRepository repository) {
this.repository = repository;
}

@Get
Flux<Author> all() { // (1)
return repository.findAll();
}

@Get("/id")
Mono<Author> get(Long id) { // (2)
return repository.findById(id);
}
}
  1. 通过返回一个可发出许多项目的响应式类型,您可以实现数据流(FlowableFlux)。
  2. 通过返回发出单个项的响应式,您可以返回整个响应(SingleMono)。

6.2.2 配置

R2DBC 驱动

Micronaut Data R2DBC 需要使用 Micronaut R2DBC 进行驱动配置。

表 1. 截至本文撰写时,可使用以下驱动

数据库依赖
H2 数据库io.r2dbc:r2dbc-h2
MySQLdev.miku:r2dbc-mysql
MariaDBorg.mariadb:r2dbc-mariadb
Postgresorg.postgresql:r2dbc-postgresql
SQL Serverio.r2dbc:r2dbc-mssql
Oraclecom.oracle.database.r2dbc:oracle-r2db

SQL 日志

您可以通过为 io.micronaut.data.query 日志器启用跟踪日志来启用 SQL 日志。例如在 logback.xml 中:

启用 SQL 查询日志记录

<logger name="io.micronaut.data.query" level="trace" />

创建模式

要创建数据库模式,建议将 Micronaut Data 与 SQL 迁移工具,如 FlywayLiquibase,配对使用。

SQL 迁移工具可为在各种数据库中创建和演进模式提供更全面的支持。

注意

大多数数据库迁移工具都使用 JDBC 驱动来更改数据库。因此,除了 R2DBC 驱动外,您可能还需要包含一个 JDBC 驱动模块,以便进行模式迁移。

如果想快速测试 Micronaut Data R2DBC,可以将数据源的 schema-generate 选项设置为 create-drop,并设置适当的模式名称:

使用 schema-generate

micronaut.application.name=example
r2dbc.datasources.default.db-type=postgresql
r2dbc.datasources.default.schema-generate=CREATE_DROP
r2dbc.datasources.default.dialect=POSTGRES
datasources.default.db-type=postgresql
datasources.default.schema-generate=CREATE_DROP
datasources.default.dialect=POSTGRES

schema-generate 选项目前只推荐用于简单的应用程序、测试和演示,并不能用于生产。配置中设置的方言是用于生成模式的方言。

设置方言

如上配置所示,您还应配置方言。虽然查询会在资源库中预先计算,但在某些情况下(如分页)仍需要指定方言。下表总结了支持的方言:

表 2. 支持的 JDBC / R2DBC 方言

方言说明
H2H2 数据库(通常用于内存测试)
MYSQLMySQL 5.5 或以上版本
POSTGRESPostgres 9.5 或更高版本
SQL_SERVERSQL Server 2012 或更高版本
ORACLEOracle 12c 或更高版本
危险

配置中的方言设置并不能取代确保在版本库中设置正确方言的需要。如果配置中的方言是 H2,那么版本库应具有 @R2dbcRepository(dialect = Dialect.H2)。因为版本库是在编译时计算的,所以当时并不知道配置值。

6.2.3 响应式仓库

下表总结了 Micronaut Data 自带的响应式仓库接口,建议与 R2DBC 一起使用:

表 1. 内置响应式资源库接口

接口描述
ReactiveStreamsCrudRepository继承 GenericRepository,并添加了返回 Publisher 的 CRUD 方法
ReactorCrudRepository继承 ReactiveStreamsCrudRepository,并使用了 Reactor 返回类型
RxJavaCrudRepository继承 GenericRepository 并添加可返回 RxJava 2 类型的 CRUD 方法
CoroutineCrudRepository继承 GenericRepository,并使用 Kotlin 例程进行反应式 CRUD 操作

6.2.4 事务

Micronaut Data R2DBC 支持响应式事务管理,例如,您可以在方法中声明 jakarta.transaction.Transactional,然后启动一个响应式事务:

package example;

import reactor.core.publisher.Mono;

import jakarta.inject.Singleton;
import jakarta.transaction.Transactional;
import java.util.Arrays;

@Singleton
public class AuthorService {
private final AuthorRepository authorRepository;
private final BookRepository bookRepository;

public AuthorService(AuthorRepository authorRepository, BookRepository bookRepository) { // (1)
this.authorRepository = authorRepository;
this.bookRepository = bookRepository;
}

@Transactional // (2)
Mono<Void> setupData() {
return Mono.from(authorRepository.save(new Author("Stephen King")))
.flatMapMany((author -> bookRepository.saveAll(Arrays.asList(
new Book("The Stand", 1000, author),
new Book("The Shining", 400, author)
))))
.then(Mono.from(authorRepository.save(new Author("James Patterson"))))
.flatMapMany((author ->
bookRepository.save(new Book("Along Came a Spider", 300, author))
)).then();
}
}
  1. 注入了支持仓库
  2. @Transactional 用于声明事务

同样的声明逻辑也可以通过注入 R2dbcOperations 接口以编程方式完成:

Flux.from(operations.withTransaction(status ->
Flux.from(authorRepository.save(new Author("Stephen King")))
.flatMap((author -> bookRepository.saveAll(Arrays.asList(
new Book("The Stand", 1000, author),
new Book("The Shining", 400, author)
))))
.thenMany(Flux.from(authorRepository.save(new Author("James Patterson"))))
.flatMap((author ->
bookRepository.save(new Book("Along Came a Spider", 300, author))
)).then()
)).collectList().block();

在上述案例中,withTransaction 方法用于启动事务。

但请注意,事务管理可能是响应式编程中最难处理的问题之一,因为您需要在响应式流程中传播事务。

大多数 R2DBC 驱动都是在 Project Reactor 中实现的,Project Reactor 能够跨响应式操作符传播上下文,Micronaut Data R2DBC 将填充此上下文,并确保事务在其中被发现时被重复使用。

不过,上下文还是很容易丢失,因为实现响应式流的不同库不会在彼此间传播上下文,所以如果包含 RxJava 或任何其他响应式运算符库,上下文很可能会丢失。

为了确保这种情况不会发生,建议将参与事务的写入操作注解为 MANDATORY,以确保在没有周围事务的情况下无法运行这些方法,这样,如果事务在响应流中丢失,就不会导致操作在单独的事务中运行:

@NonNull
@Override
@Transactional(Transactional.TxType.MANDATORY)
<S extends Book> Publisher<S> save(@NonNull @Valid @NotNull S entity);

@NonNull
@Override
@Transactional(Transactional.TxType.MANDATORY)
<S extends Book> Publisher<S> saveAll(@NonNull @Valid @NotNull Iterable<S> entities);

如果事务在响应流程中丢失,有几种方法可以解决问题。一种方法是使用 R2dbcOperations 接口的 withTransaction 方法获取当前的 ReactiveTransactionStatus,然后将此实例传递到另一个 withTransaction 方法的执行中,或者直接将其作为最后一个参数传递给在仓库本身声明的任何方法。

下面是前一种方法的示例,这次使用的 RxJava 2 会导致传播损失:

Flux.from(operations.withTransaction(status -> // (1)
Flux.from(authorRepository.save(new Author("Michael Crichton")))
.flatMap((author -> operations.withTransaction(status, (s) -> // (2)
bookRepository.saveAll(Arrays.asList(
new Book("Jurassic Park", 300, author),
new Book("Disclosure", 400, author)
)))))
)).collectList().block();
  1. 外部 withTransaction 调用启动事务
  2. 内部调用确保现有事务得到传播

6.2.5 响应式实体事件

Micronaut Data R2DBC 支持在 Micronaut Data 2.3 及以上版本中引入的持久化事件,但是应该注意的是,这些事件不应该阻塞,只应该执行不引起任何网络I/O的操作,如果执行了这些操作,应该有一个新的线程来执行这个逻辑。

请注意,持久化事件最常用于在执行插入之前预填充数据库属性(例如对密码等进行编码),这些类型的操作通常不涉及阻塞 I/O,可以安全执行。

6.3 仓库

在 Micronaut Data 中的快速入门 JDBC / R2DBC 资源库被定义为使用 @JdbcRepositoryannotation@R2dbcRepository 进行注解的接口。

在多数据源场景中,@Repository@Transactional 注解可用于指定要使用的数据源配置。默认情况下,Micronaut Data 会查找默认数据源。

例如:

@JdbcRepository(dialect = Dialect.ORACLE, dataSource = "inventoryDataSource") (1)
@io.micronaut.transaction.annotation.Transactional("inventoryDataSource") (2)
public interface PhoneRepository extends CrudRepository<Phone, Integer> {
Optional<Phone> findByAssetId(@NotNull Integer assetId);
}
  1. @JdbcRepository 具有特定方言和数据源配置 "inventoryDataSource
  2. 事务注解,指向数据源配置 "inventoryDataSource

根据方法签名或为 GenericRepository 接口指定的通用类型参数,可以确定将哪个实体作为根实体进行查询。

如果无法确定根实体,则会出现编译错误。

JDBC 也支持 JPA 实现所支持的相同接口。

请注意,由于查询是在编译时计算的,因此必须在仓库中指定所使用的 dialect

提示

建议针对目标方言进行测试。测试容器项目就是一个很好的解决方案。如果必须针对另一种方言(如 H2)进行测试,则可以定义一个子接口,在测试范围内用不同的方言 @Replaces 仓库。

请注意,除了接口,您还可以将仓库定义为抽象类:

package example;

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.jdbc.runtime.JdbcOperations;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;

import jakarta.transaction.Transactional;
import java.sql.ResultSet;
import java.util.List;
import java.util.stream.Collectors;

@JdbcRepository(dialect = Dialect.H2)
public abstract class AbstractBookRepository implements CrudRepository<Book, Long> {

private final JdbcOperations jdbcOperations;

public AbstractBookRepository(JdbcOperations jdbcOperations) {
this.jdbcOperations = jdbcOperations;
}

@Transactional
public List<Book> findByTitle(String title) {
String sql = "SELECT * FROM Book AS book WHERE book.title = ?";
return jdbcOperations.prepareStatement(sql, statement -> {
statement.setString(1, title);
ResultSet resultSet = statement.executeQuery();
return jdbcOperations.entityStream(resultSet, Book.class).collect(Collectors.toList());
});
}
}

从上面的示例中可以看出,使用抽象类是非常有用的,因为它允许你结合自定义代码来执行自己的 SQL 查询。

上面的示例使用了 JdbcOperations 接口,它简化了在事务上下文中执行 JDBC 查询的过程。

提示

您还可以注入任何其他工具来处理更复杂的查询,如 QueryDSL、JOOQ、Spring JdbcTemplate 等。

6.3.1 访问数据

与 JPA/Hibernate 不同,Micronaut Data JDBC / R2DBC 是无状态的,没有需要状态管理的持久化会话概念。

由于没有会话,所以不支持脏检查等功能。这对定义插入和更新的存储库方法有影响。

默认情况下,当使用 save(MyEntity) 等方法保存实体时,总是执行 SQL INSERT,因为 Micronaut Data 无法知道实体是否与特定会话相关联。

如果你想更新一个实体,你应该使用 update(MyEntity),或者定义一个合适的更新方法,只更新你想更新的数据:

void update(@Id Long id, int pages);

void update(@Id Long id, String title);

通过将方法明确定义为更新方法,Micronaut Data 知道要执行 UPDATE

6.3.2 乐观锁定

乐观锁定是一种策略,即注意实际记录状态的版本,只有当版本相同时才修改记录。

要为实体启用乐观锁定,请添加以下类型之一的 @Version 注解字段:

  • java.lang.Integer
  • java.lang.Long
  • java.lang.Short
  • 扩展了 java.time.Temporal 的日期时间类型

该字段将在更新操作中递增(对于数字类型)或替换(对于日期类型)。

Micronaut Data 将生成版本匹配的 UPDATE/DELETE SQL 查询:... WHERE rec.version = :currentVersion ... 如果更新/删除没有产生任何结果,将抛出 OptimisticLockException

@Entity
public class Student {

@Id
@GeneratedValue
private Long id;
@Version
private Long version;

可以在部分更新或删除方法中使用 @Version,在这种情况下,版本必须与存储记录的版本相匹配。

@Repository
public interface StudentRepository extends CrudRepository<Student, Long> {

void update(@Id Long id, @Version Long version, String name);

void delete(@Id Long id, @Version Long version);
}

6.3.3 悲观锁定

通过使用 find*ForUpdate 方法支持悲观锁定。

@JdbcRepository(dialect = Dialect.POSTGRES)
public interface AccountBalanceRepository extends CrudRepository<AccountBalance, Long> {

AccountBalance findByIdForUpdate(Long id); (1)

@Transactional (2)
void addToBalance(Long id, BigInteger amount) {
AccountBalance accountBalance = findByIdForUpdate(id); (3)
accountBalance.addAmount(amount);
update(accountBalance); (4)
}
}
  1. ForUpdate 后缀表示应锁定所选记录。
  2. 读取和写入操作都封装在一个事务中。
  3. 执行锁定读取,防止其他查询访问记录。
  4. 安全地更新记录。

所有 find 方法都可以声明为 ForUpdate

@JdbcRepository(dialect = Dialect.POSTGRES)
public interface BookRepository extends CrudRepository<Book, Long> {

@Join("author")
Optional<Book> findByIdForUpdate(Long id);

List<Book> findAllOrderByTotalPagesForUpdate();

List<Book> findByTitleForUpdate(String title);
}

为这些方法生成的查询会使用 FOR UPDATE SQL 子句,或在 SQL Server 中使用 UPDLOCKROWLOCK 查询提示。

警告

FOR UPDATE 子句的语义可能因数据库而异。请务必查看引擎的相关文档。

6.4 带有标准 API 的仓库

在某些情况下,你需要在运行时以编程方式建立一个查询;为此,Micronaut Data实现了Jakarta Persistence Criteria API 3.0的一个子集,它可用于Micronaut Data JDBC和R2DBC功能。

为了实现无法在编译时定义的查询,Micronaut Data 引入了 JpaSpecificationExecutor 仓库接口,可用于扩展您的仓库接口:

@JdbcRepository(dialect = Dialect.H2)
public interface PersonRepository extends CrudRepository<Person, Long>, JpaSpecificationExecutor<Person> {
}

每种方法都需要一个“规范”,它是一个功能接口,包含一组旨在以编程方式建立查询的标准 API 对象。

Micronaut 标准 API 目前只实现了 API 的一个子集。其中大部分在内部用于创建带有谓词和投影的查询。

目前,不支持 JPA Criteria API 功能:

  • 使用自定义 ON 表达式和类型化连接方法(如 joinSet 等)进行连接
  • 子查询
  • 集合操作:isMember
  • 自定义或元组结果类型
  • 转换表达式,如 concat、substring 等
  • 案例和函数

有关 Jakarta Persistence Criteria API 3.0 的更多信息,参阅官方 API 规范

6.4.1 查询

要查找一个或多个实体,您可以使用 JpaSpecificationExecutor 接口中的以下方法之一:

Optional<Person> findOne(PredicateSpecification<Person> spec);

Optional<Person> findOne(QuerySpecification<Person> spec);

List<Person> findAll(PredicateSpecification<Person> spec);

List<Person> findAll(QuerySpecification<Person> spec);

List<Person> findAll(PredicateSpecification<Person> spec, Sort sort);

List<Person> findAll(QuerySpecification<Person> spec, Sort sort);

Page<Person> findAll(PredicateSpecification<Person> spec, Pageable pageable);

Page<Person> findAll(QuerySpecification<Person> spec, Pageable pageable);

如你所见,findOne/findAll 方法有两种变体。

第一个方法是期待 PredicateSpecification,这是一个简单的规范接口,可以通过实现它来返回一个谓词:

import static jakarta.persistence.criteria.*;

public interface PredicateSpecification<T> {

(1)
@Nullable
Predicate toPredicate(@NonNull Root<T> root, (2)
@NonNull CriteriaBuilder criteriaBuilder (3)
);

}
  1. 该规范正在生成一个查询限制谓词
  2. 实体根
  3. 标准生成器

该接口还可用于更新和删除方法,并提供了 orand 方法,用于组合多个谓词。

第二个接口仅用于查询标准,因为它包含 jakarta.persistence.criteria.CriteriaQuery 作为参数。

import static jakarta.persistence.criteria.*;

public interface QuerySpecification<T> {

(1)
@Nullable
Predicate toPredicate(@NonNull Root<T> root, (2)
@NonNull CriteriaQuery<?> query, (3)
@NonNull CriteriaBuilder criteriaBuilder (4)
);

}
  1. 该规范正在生成一个查询限制谓词
  2. 实体根
  3. 标准查询实例
  4. 标准生成器

实现计数查询可使用以下方法:

long count(PredicateSpecification<Person> spec);

long count(QuerySpecification<Person> spec);

您可以定义有助于创建查询的标准规范方法:

class Specifications {

static PredicateSpecification<Person> nameEquals(String name) {
return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get(Person_.name), name);
}

static PredicateSpecification<Person> longNameEquals(String longName) {
return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get(Person_.longName), longName);
}

static PredicateSpecification<Person> ageIsLessThan(int age) {
return (root, criteriaBuilder) -> criteriaBuilder.lessThan(root.get(Person_.age), age);
}

}

然后,您可以将它们组合起来进行 findcount 查询:

Person denis = personRepository.findOne(nameEquals("Denis")).orElse(null);

Person josh = personRepository.findOne(longNameEquals("Josh PM")).orElse(null);

long countAgeLess30 = personRepository.count(ageIsLessThan(30));

long countAgeLess20 = personRepository.count(ageIsLessThan(20));

long countAgeLess30NotDenis = personRepository.count(ageIsLessThan(30).and(not(nameEquals("Denis"))));

List<Person> people = personRepository.findAll(where(nameEquals("Denis").or(nameEquals("Josh"))));
注意

示例使用的是编译时已知的值,在这种情况下,最好创建自定义存储库方法,这样就可以在编译时生成查询,消除运行时的开销。建议仅在动态查询中使用标准,因为动态查询的结构在构建时是未知的。

6.4.2 更新

要实现更新,您可以使用 JpaSpecificationExecutor 接口中的以下方法:

long updateAll(UpdateSpecification<Person> spec);

该方法期待 UpdateSpecification,它是规范接口的一个变体,包括对 jakarta.persistence.criteria.CriteriaUpdate 的访问:

import static jakarta.persistence.criteria.*;

public interface UpdateSpecification<T> {

(1)
@Nullable
Predicate toPredicate(@NonNull Root<T> root, (2)
@NonNull CriteriaUpdate<?> query, (3)
@NonNull CriteriaBuilder criteriaBuilder (4)
);

}
  1. 该规范正在生成一个查询限制谓词
  2. 实体根
  3. 标准更新实例
  4. 标准生成器

可使用 jakarta.persistence.criteria.CriteriaUpdate 接口更新特定属性:

query.set(root.get(Person_.name), newName);

您可以定义标准规范方法,包括更新规范,这将有助于您创建更新查询:

class Specifications {

static PredicateSpecification<Person> nameEquals(String name) {
return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get(Person_.name), name);
}

static PredicateSpecification<Person> longNameEquals(String longName) {
return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get(Person_.longName), longName);
}

static PredicateSpecification<Person> ageIsLessThan(int age) {
return (root, criteriaBuilder) -> criteriaBuilder.lessThan(root.get(Person_.age), age);
}

static UpdateSpecification<Person> setNewName(String newName) {
return (root, query, criteriaBuilder) -> {
query.set(root.get(Person_.name), newName);
return null;
};
}
}

然后,您可以使用更新规范与谓词规范相结合:

long recordsUpdated = personRepository.updateAll(setNewName("Steven").where(nameEquals("Denis")));

6.4.3 删除

要删除一个实体或多个实体,可以使用 JpaSpecificationExecutor 接口中的以下方法之一:

long deleteAll(PredicateSpecification<Person> spec);

long deleteAll(DeleteSpecification<Person> spec);

与查询一样,deleteAll 方法也有两种变体。

第一个方法期待 PredicateSpecification,它与查询部分描述的接口相同。

第二种方法带有 DeleteSpecification,仅用于删除标准,因为它包含了对 jakarta.persistence.criteria.CriteriaDelete 的访问。

import static jakarta.persistence.criteria.*;

public interface DeleteSpecification<T> {

(1)
@Nullable
Predicate toPredicate(@NonNull Root<T> root, (2)
@NonNull CriteriaDelete<?> query, (3)
@NonNull CriteriaBuilder criteriaBuilder (4)
);

}
  1. 该规范正在生成一个查询限制谓词
  2. 实体根
  3. 标准删除实例
  4. 标准生成器

对于删除,您可以重复使用与查询和更新相同的谓词:

class Specifications {

static PredicateSpecification<Person> nameEquals(String name) {
return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get(Person_.name), name);
}

static PredicateSpecification<Person> longNameEquals(String longName) {
return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get(Person_.longName), longName);
}

static PredicateSpecification<Person> ageIsLessThan(int age) {
return (root, criteriaBuilder) -> criteriaBuilder.lessThan(root.get(Person_.age), age);
}

}

只需将谓词规范传递给 deleteAll 方法即可:

long recordsDeleted = personRepository.deleteAll(where(nameEquals("Denis")));

6.4.4 其他仓库变化

Micronaut Data 包含不同的规范执行器接口变体,旨在与异步或反应式版本库一起使用。

表 1. JpaSpecificationExecutor 仓库接口的内置变体

接口描述
JpaSpecificationExecutor查询、删除和更新数据的默认接口
AsyncJpaSpecificationExecutor规格库的异步版本
ReactiveStreamsJpaSpecificationExecutor规范库的响应流--Publisher<> 版本
ReactorJpaSpecificationExecutor规范库 Reactor 版本
CoroutineJpaSpecificationExecutor使用例程的 Kotlin 版本接口

6.4.5 类型安全的 Java 查询

Jakarta Persistence 标准 API 通过使用编译时生成的静态元模型支持类型安全查询。

例如,实体 MyEntity 将生成一个名称为 MyEntity_ 的相应元模型实体,它将与原始实体位于同一个包中。新生成实体中的每个字段都将与实体的属性相对应,并可用作属性引用。

官方 API 规范中的示例:

CriteriaBuilder cb = ...
CriteriaQuery<String> q = cb.createQuery(String.class);
Root<Customer> customer = q.from(Customer.class);
Join<Customer, Order> order = customer.join(Customer_.orders);
Join<Order, Item> item = order.join(Order_.lineItems);
q.select(customer.get(Customer_.name))
.where(cb.equal(item.get(Item_.product).get(Product_.productType), "printer"));

请注意,到目前为止,您还不能使用 Micronaut Data 注解(io.micronaut.data.annotation 包中的注解)来生成静态 JPA 元数据,唯一支持的方法是使用 Jakarta Persistence 注解(位于 jakarta.persistence 包中)与 Hibernate JPA 静态元模型生成器相结合,即使在运行时您实际上没有使用 Hibernate,而是使用 Micronaut Data JDBC,该生成器也会生成元模型。

要配置元模型生成器,只需在注解处理器类路径中添加以下依赖即可:

annotationProcessor("org.hibernate:hibernate-jpamodelgen-jakarta")
注意

需要使用 Hibernate 6 版本的 hibernate-jpamodelgen-jakarta,因为之前版本的 Hibernate 仍在使用 javax.persistence 包。

我们需要在 Java classpath 中包含生成的类,以便它们可以被访问:

Gradle 构建示例:

sourceSets {
generated {
java {
srcDirs = ["$build/generated/java"]
}
}
}

如果一切设置正确,您就可以在集成开发环境代码完成中看到生成的元模型类,并可以使用它们:

static PredicateSpecification<Person> nameEquals(String name) {
return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get(Person_.name), name);
}

static PredicateSpecification<Person> longNameEquals(String longName) {
return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get(Person_.longName), longName);
}

static PredicateSpecification<Person> ageIsLessThan(int age) {
return (root, criteriaBuilder) -> criteriaBuilder.lessThan(root.get(Person_.age), age);
}

static UpdateSpecification<Person> setNewName(String newName) {
return (root, query, criteriaBuilder) -> {
query.set(root.get(Person_.name), newName);
return null;
};
}

static PredicateSpecification<Product> manufacturerNameEquals(String name) {
return (root, cb) -> cb.equal(root.join(Product_.manufacturer).get(Manufacturer_.name), name);
}

static PredicateSpecification<Product> joined() {
return (root, cb) -> {
root.join("manufacturer");
return null;
};
}
注意

有关静态元模型的更多信息,参阅官方规范

6.5 映射实体

正如快速入门部分所提到的,如果你需要自定义实体如何映射到数据库的表和列名称,你可以使用 JPA 注解或 Micronaut Data 自己的 io.micronaut.data.annotation 包中的注解来实现。

Micronaut Data JDBC / R2DBC 的一个重要方面是,无论使用 JPA 注解还是 Micronaut Data 注解,实体类都必须与 Micronaut Data 一起编译。

这是因为Micronaut Data在编译时预先计算了持久化模型(实体之间的关系、类/属性名称到表/列名称的映射),这也是 Micronaut Data JDBC 能快速启动的原因之一。

下面是一个使用 Micronaut Data 注解进行映射的例子:

Micronaut Data 注解映射示例

/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.data.tck.entities;

import io.micronaut.data.annotation.AutoPopulated;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.Relation;

import java.util.Set;
import java.util.UUID;

@MappedEntity
public class Country {

@Id
@AutoPopulated
private UUID uuid;
private String name;

@Relation(value = Relation.Kind.ONE_TO_MANY, mappedBy = "country")
private Set<CountryRegion> regions;

public Country(String name) {
this.name = name;
}

public String getName() {
return name;
}

public UUID getUuid() {
return uuid;
}

public void setUuid(UUID uuid) {
this.uuid = uuid;
}

public Set<CountryRegion> getRegions() {
return regions;
}

public void setRegions(Set<CountryRegion> regions) {
this.regions = regions;
}
}

6.5.1 SQL 注解

下表总结了不同的注解及其功能。如果您熟悉并喜欢 JPA 注释,请跳至下一节:

表 1. Micronaut 数据注解

注解描述
@AutoPopulated应由 Micronaut Data 自动填充的值的元注释(如时间戳和 UUID)
@DateCreated允许在插入前分配数据创建值(如 java.time.Instant
@DateUpdated允许在插入或更新之前分配最后更新值(如 java.time.Instant)
@Embeddable指定 bean 是可嵌入的
@EmbeddedId指定实体的嵌入式 ID
@GeneratedValue指定属性值由数据库生成,不包含在插入中
@JoinTable指定连接表关联
@JoinColumn指定连接列映射
@Id指定实体的 ID
@MappedEntity指定映射到数据库的实体。如果表名与实体名不同,请将表名作为 value 传入。例如 @MappedEntity( value = "TABLE_NAME" )
@MappedProperty用于自定义列名、定义和数据类型
@Relation用于指定关系(一对一、一对多等)。
@Transient用于指定一个属性为瞬时属性
@TypeDef用于指定属性的数据类型和自定义转换器
@Version指定实体的版本字段,启用乐观锁定

在使用 JPA 的情况下,只支持以下注解的子集:

  • 基本注解 @Table @Id @Version @Column @Transient @Enumerated
  • 嵌入式定义:@Embedded @EmbeddedId @Embeddable
  • 关系映射:@OneToMany @OneToOne @ManyToMany
  • 连接规范:@JoinTable @JoinColumn
  • 类型转换器: @Convert @ConverterAttributeConverter 接口
注意

Micronaut Data 支持 javax.persistencejakarta.persistence 包。

Micronaut Data JDBC / R2DBC 也不是一个 ORM,而是一个简单的数据映射器,因此 JPA 中的许多概念并不适用,不过对于熟悉这些注解的用户来说,使用它们还是很方便的。

6.5.2 可扩展查询

在某些情况下,查询需要扩展以容纳所有参数的值。参数为集合或数组的查询:WHERE value IN (?) 的查询将扩展为 WHERE value IN (?, ?, ?, ?)

如果其中一个参数是可扩展的,Micronaut Data 将在构建时存储查询的附加信息,这样就不需要在运行时解析查询了。

默认情况下,所有扩展了 java.lang.Iterable 类型的参数都是自动可扩展的。您可以使用 @Expandable 注解将参数标记为可扩展,例如,如果参数是一个数组,您可能就需要这样做。

注意

如果目标数据库支持数组类型,最好使用数组类型。例如,在 Postgres 中,可以使用 WHERE value = ANY (:myValues) ,其中 myValues 的类型是 @TypeDef(type=DataType.STRING_ARRAY)。

6.5.3 ID 生成

默认 ID 生成期望数据库为 ID 填充一个值,如 IDENTITY 列。

您可以移除 @GeneratedValue 注解,在这种情况下,我们希望您在调用 save() 之前为 ID 赋值。

如果希望使用序列来指定 ID,则应调用生成序列值的 SQL,并在调用 save() 之前进行指定。

通过添加注有 @Id@AutoPopulated 的属性,也可支持自动分配的 UUID。

6.5.4 复合主键

可以使用 JPA 或 Micronaut 数据注解定义复合主键。复合 ID 需要一个额外的类来表示该键。该类应定义与组成复合键的列相对应的字段。例如:

package example;

import jakarta.persistence.Embeddable;
import java.util.Objects;

@Embeddable
public class ProjectId {
private final int departmentId;
private final int projectId;

public ProjectId(int departmentId, int projectId) {
this.departmentId = departmentId;
this.projectId = projectId;
}

public int getDepartmentId() {
return departmentId;
}

public int getProjectId() {
return projectId;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ProjectId projectId1 = (ProjectId) o;
return departmentId == projectId1.departmentId &&
projectId == projectId1.projectId;
}

@Override
public int hashCode() {
return Objects.hash(departmentId, projectId);
}
}
提示

建议 ID 类不可变,并实现 equals/hashCode。提示:使用 Java 时,请务必为组成复合键的字段定义获取器。

然后,您可以使用 JPA 的 @EmbeddedId@EmbeddedId 声明实体的 id 属性:

package example;

import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;

@Entity
public class Project {
@EmbeddedId
private ProjectId projectId;
private String name;

public Project(ProjectId projectId, String name) {
this.projectId = projectId;
this.name = name;
}

public ProjectId getProjectId() {
return projectId;
}

public String getName() {
return name;
}
}
提示

要更改 ID 的列映射,可在 ProjectId 类中的字段上使用 @Column 注解。

6.5.5 构造函数参数

Micronaut Data JDBC / R2DBC 还允许使用构造函数参数而不是 getters/setters 来定义不可变对象。如果您定义了多个构造函数,那么用于从数据库创建对象的构造函数应使用 io.micronaut.core.annotation.Creator 注解。

例如:

package example;

import io.micronaut.core.annotation.Creator;

import jakarta.persistence.*;

@Entity
public class Manufacturer {
@Id
@GeneratedValue
private Long id;
private String name;

@Creator
public Manufacturer(String name) {
this.name = name;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

}

从上面的示例中可以看出,对象的 ID 应包含一个设置器,因为它必须从数据库生成的值中分配。

6.5.6 SQL 命名策略

将驼峰大小写的类名和属性名转换为数据库表和列时,默认的命名策略是使用下划线分隔的小写字母。换句话说,FooBar 变成了 foo_bar

如果不满意,可以通过设置实体上 @MappedEntity 注解的 namingStrategy 成员来定制:

Micronaut 数据命名策略

@MappedEntity(namingStrategy = NamingStrategies.Raw.class)
public class CountryRegion {
...
}

需要注意的几个重要事项。由于 Micronaut Data 会在编译时预先计算表和列的名称映射,因此指定的 NamingStrategy 实现必须位于注解处理器类路径(Java 为 annotationProcessor 作用域,Kotlin 为 kapt)上。

如果在本地镜像中运行项目,自定义命名策略需要有 io.micronaut.core.annotation.TypeHint(CustomNamingStrategy.class) 注解,其中自定义命名策略类是 CustomNamingStrategy

此外,如果不想在每个实体上重复上述注解定义,定义元注解也很方便,在元注解中,上述注解定义会应用到添加到类中的另一个注解。

转义表/列名称标识符

在某些情况下,如果表名和/或列名中使用的字符在不转义的情况下无效,则有必要转义表名和/或列名。

在这种情况下,应将 @MappedEntity 注解的转义成员设置为 true

@MappedEntity(escape=true)

Micronaut Data 将生成 SQL 语句,在查询中使用适合配置的 SQL 方言的转义字符转义表和列名称。

覆盖默认查询别名

默认查询别名是表名后跟一个下划线。如果要更改,请在 @MappedEntity 注解中指定:

@MappedEntity(alias="my_table_")

6.5.7 关联映射

要指定两个实体之间的关系,需要使用 @Relation 注解。关系类型是使用枚举 @Kind value 属性指定的,它与 JPA 关系注解名称(@OneToMany@OneToOne 等)类似。

表 1. Micronaut Data 支持的关系:

Kind描述
Kind.ONE_TO_MANY一对多关联
Kind.ONE_TO_ONE一对一关联
Kind.MANY_TO_MANY多对多关联
Kind.MANY_TO_ONE多对一关联
Kind.EMBEDDED嵌入式关联

使用 "mappedBy" 指定此关系映射的逆属性。

表 2. Micronaut Data 支持的关联级联类型:

类型描述
Cascade.PERSIST保存拥有的实体时,相关的一个或多个实体将被持久化
Cascade.UPDATE更新拥有的实体时,将更新关联实体
Cascade.NONE(默认)不进行级联操作
Cascade.ALL所有(Cascade.PERSISTCascade.UPDATE)操作都是级联的
注意

您可以使用 JPA 的等价注解 @JoinTable@JoinColumn 来指定更复杂的映射定义。

6.5.8 关联检索

Micronaut Data 是一个简单的数据映射器,因此它不会使用单端关联的实体代理懒加载等技术为你获取任何关联。

您必须提前指定要获取的数据。您不能将关联映射为 eager 或 lazy。这种设计选择的原因很简单,即使在 JPA 世界中,由于 N+1 查询问题,访问懒关联或懒初始化集合也被认为是不好的做法,建议总是编写优化的连接查询。

Micronaut Data JDBC / R2DBC 在此基础上更进一步,干脆不支持那些被认为是糟糕做法的功能。不过,这确实会影响您如何为关联建模。例如,如果您在构造函数参数中定义关联,如以下实体:

package example;

import jakarta.persistence.*;

@Entity
public class Product {

@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
private Manufacturer manufacturer;

public Product(String name, Manufacturer manufacturer) {
this.name = name;
this.manufacturer = manufacturer;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public Manufacturer getManufacturer() {
return manufacturer;
}
}

然后,在未指定连接的情况下尝试读回 Product 实体,就会出现异常,因为 manufacturer 关联不是 Nullable

有几种方法可以解决这个问题,一种方法是在存储库级别声明始终获取 manufacturer,另一种方法是在 manufacturer 参数上声明 @Nullable 注解,允许将其声明为 null(或者在 Kotlin 中,在构造函数参数名称的末尾添加 ?)选择哪种方法取决于应用程序的设计。

下一节将提供更多有关处理连接的内容。

6.5.9 使用 @ColumnTransformer

受 Hibernate 中类似注解的启发,您可以使用 @ColumnTransformer 注解在从数据库读取或向数据库写入列时应用转换。

此功能可用于加密/解密值或调用任意数据库函数。要定义读取转换,请使用 read 成员。例如

应用读取转换

@ColumnTransformer(read = "UPPER(@.name)")
private String name;
注意

@ 是查询别名占位符,如果查询指定了该别名,则将用该别名替换。例如 "UPPER(@.name) 将变为 UPPER(project_.name)

要应用写入转换,应使用写入成员,并包含一个 ? 占位符:

应用写转换

@ColumnTransformer(write = "UPPER(?)")
private String name;

在这种情况下生成的任何 INSERTUPDATE 语句都将包含上述写入条目。

6.5.10 使用 @MappedProperty 别名

如果需要在结果集中以自定义名称返回列名,可以使用 @MappedProperty 注解中的 alias 属性。

例如,对于查询结果中可能过长的遗留列(与表别名相结合时可能超过列的最大长度),该别名属性可能非常有用。

package example;

import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedProperty;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;


@Entity
public class Person {
@Id
@GeneratedValue
private Long id;
private String name;
private int age;
@MappedProperty(value = "long_name_column_legacy_system", alias = "long_name")
private String longName;

public Person() {
}

public Person(String name, int age, String longName) {
this(null, name, age, longName);
}

public Person(Long id, String name, int age, String longName) {
this.id = id;
this.name = name;
this.age = age;
this.longName = longName;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getLongName() {
return longName;
}

public void setLongName(String longName) {
this.longName = longName;
}
}

在本例中,数据库将以 long_name 的形式在结果中返回原始列名 long_name_column_legacy_system。如果设置了 alias 属性,那么在编写自定义或本地查询时应注意按照 alias 值返回字段。

在关联的 MappedProperty 中设置 alias 不会产生影响,因为它只对字段/列映射有意义。

6.5.11 支持 JSON 列

您可以使用 @TypeDef 注解将一个类的字段声明为 JSON 类型,如下所示:

@TypeDef(type = DataType.JSON)
private Map<String, String> data;

上述内容将映射到名为 data 的列。根据数据库的不同,列类型也会有所调整。例如,对于支持本地 JSON 的 Postgres,列类型将是 JSONB

注意

要允许在实体属性中序列化和反序列化 JSON,必须在 classpath 中包含 Jackson 和 micronaut-runtime 模块。

6.5.12 JSON 视图

从 Micronaut Data 4.0 和 Oracle23c 数据库开始,一个实体可以如下方式映射到一个 JSON VIEW:

@JsonView("CONTACT_VIEW")
public class ContactView

其中 "CONTACT_VIEW" 是数据库中 duality json 视图对象的实际名称。目前只有 Oracle 数据库从 23c 版本开始支持该功能。有关 Oracle JSON VIEW 的更多信息,请访问 https://docs.oracle.com/en/database/oracle/oracle-database/23/jsnvu/overview-json-relational-duality-views.html。

从本质上讲,json 视图将被视为映射实体,从数据库返回 JSON 结构并映射到 java 实体。所有 CRUD 操作都可针对 json 视图映射实体执行。

限制

  • 在模式创建过程中,json 视图映射实体将被跳过,用户应手动或通过迁移脚本创建这些实体

6.5.13 支持 Java 16 记录

自 2.3.0 起,Micronaut Data JDBC / R2DBC 支持使用 Java 16。

下面的记录类演示了这一功能:

  1. 在记录上使用 @MappedEntity 注解
  2. 数据库标识符使用 @Id@GeneratedValue 注解,并标记为 @Nullable

由于记录是不可变的,因此需要将生成值的构造函数参数标记为 @Nullable,并为这些参数传递 null。下面是一个示例:

需要注意的是,返回的实例与传递给 save 方法的实例不同。当执行写入操作时,Micronaut Data 将使用复制构造函数方法来填充数据库标识符,并从保存方法中返回一个新实例。

6.5.14 支持 Kotlin 不可变数据类

Micronaut Data JDBC / R2DBC 支持使用不可变的 Kotlin 数据类作为模型实体。其实现与 Java 16 记录相同:修改实体时将使用复制构造函数,每次修改都意味着一个新的实体实例。

src/main/kotlin/example/Student.kt

package example

import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.data.annotation.Relation

@MappedEntity
data class Student(
@field:Id @GeneratedValue
val id: Long?,
val name: String,
@Relation(value = Relation.Kind.MANY_TO_MANY, cascade = [Relation.Cascade.PERSIST])
val courses: List<Course>,
@Relation(value = Relation.Kind.ONE_TO_MANY, mappedBy = "student")
val ratings: List<CourseRating>
) {
constructor(name: String, items: List<Course>) : this(null, name, items, emptyList())
}
注意

在实体初始化过程中无法创建的生成值和关系应声明为可空值。

6.6 数据类型

Micronaut Data JDBC / R2DBC 支持大多数常见的 Java 数据类型。默认支持以下属性类型:

  • 所有原生类型及其封装(intjava.lang.Integer 等)
  • CharSequenceString
  • 日期类型,如 java.util.Datejava.time.LocalDate 等。
  • 枚举类型(仅按名称)
  • 实体引用。在 @ManyToOne 的情况下,外键列名的计算方法是关联名称加上 _id 后缀。你可以用 @Column(name 或提供 NamingStrategy.mappedName(..)` 实现来改变这种情况。
  • 实体集合。在 @OneToMany 的情况下,如果指定了 mappedBy,则预计存在定义列的逆属性,否则将创建连接表映射。

如果想定义自定义数据类型,可以通过定义一个注有 @TypeDef 的类来实现。

6.7 使用属性转换器

在某些情况下,您希望以不同于实体的方式在数据库中表示属性。

请看下面的实体示例:

package example;

import jakarta.persistence.*;

@Entity
public class Sale {

@ManyToOne
private final Product product;
private final Quantity quantity;

@Id
@GeneratedValue
private Long id;

public Sale(Product product, Quantity quantity) {
this.product = product;
this.quantity = quantity;
}

public Product getProduct() {
return product;
}

public Quantity getQuantity() {
return quantity;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}
}

Sale 类有一个 Quantity 类型的引用。Quantity 类型定义如下:

package example;

import io.micronaut.data.annotation.TypeDef;
import io.micronaut.data.model.DataType;

@TypeDef(type = DataType.INTEGER, converter = QuantityAttributeConverter.class)
public class Quantity {

private final int amount;

private Quantity(int amount) {
this.amount = amount;
}

public int getAmount() {
return amount;
}

public static Quantity valueOf(int amount) {
return new Quantity(amount);
}
}

正如您所看到的,@TypeDef 用于使用 DataType 枚举将 Quantity 类型定义为 INTEGER

提示

如果不能直接在类型上声明 @TypeDef,那么可以在使用该类型的字段上声明。

最后一步是添加自定义属性转换,以便 Micronaut Data 知道如何读写 Integer 类型:

package example;

import io.micronaut.core.convert.ConversionContext;
import io.micronaut.data.model.runtime.convert.AttributeConverter;
import jakarta.inject.Singleton;

@Singleton // (1)
public class QuantityAttributeConverter implements AttributeConverter<Quantity, Integer> {

@Override // (2)
public Integer convertToPersistedValue(Quantity quantity, ConversionContext context) {
return quantity == null ? null : quantity.getAmount();
}

@Override // (3)
public Quantity convertToEntityValue(Integer value, ConversionContext context) {
return value == null ? null : Quantity.valueOf(value);
}

}
  1. 属性转换器实现 @AttributeConverter 且必须是一个 Bean
  2. QuantityInteger 的转换器
  3. IntegerQuantity 的转换器
注意

可以使用 @MappedProperty 来定义转换器:@MappedProperty(converter = QuantityTypeConverter.class),在这种情况下,数据类型将被自动检测到。

6.8 连接查询

如上一节所述,Micronaut Data JDBC 不支持传统 ORM 意义上的关联。没有懒加载或代理支持。

考虑上一节中与 Manufacturer 实体有关联的 Product 实体:

package example;

import io.micronaut.core.annotation.Creator;

import jakarta.persistence.*;

@Entity
public class Manufacturer {
@Id
@GeneratedValue
private Long id;
private String name;

@Creator
public Manufacturer(String name) {
this.name = name;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

}

如果您查询 Product 实例,默认情况下 Micronaut Data JDBC 将只查询和获取简单的属性。对于像上面这样的单端关联,Micronaut Data 将只检索 ID 并在可能的情况下分配它(对于需要构造函数参数的实体,这甚至是不可能的)。

如果还需要获取关联,那么可以在存储库接口上使用 @Join 注解,指定执行 INNER JOIN(或更合适的连接类型)来获取关联 Manufacturer

@JdbcRepository(dialect = Dialect.H2)
public interface ProductRepository extends CrudRepository<Product, Long> {
@Join(value = "manufacturer", type = Join.Type.FETCH)
// (1)
List<Product> list();
}
  1. @Join 用于表示应包含 INNER JOIN 子句。

请注意,@Join 注解是可重复的,因此可以为不同的关联多次指定。此外,注解的 type 成员可用于指定连接类型,例如 LEFTINNERRIGHT

最后,默认情况下,Micronaut Data 会生成别名,用于在连接和查询中选择列。但是,如果在任何时候遇到冲突,可以使用 @Join 注解的 alias 成员为特定连接指定别名。您可以使用 @MappedEntity 注解的别名成员覆盖默认实体别名。

警告

某些数据库(如 Oracle)会限制 SQL 查询中别名的长度,因此您可能希望设置自定义别名的另一个原因是避免超过 Oracle 中的别名长度限制。

如果需要进行比 Micronaut Data 提供的连接选项更复杂的操作,则可能需要使用本地查询。

6.9 显式查询

当与 JDBC 一起使用 Micronaut Data 时,你可以使用 @Query 注解执行本地 SQL 查询:

@Query("select * from book b where b.title like :title limit 5")
List<Book> findBooks(String title);

上述示例将针对数据库执行原始 SQL。

注意

对于返回一个 PagePagination 查询,还需要指定本地 countQuery

显式查询和连接

在编写显式 SQL 查询时,如果您在查询中指定了任何连接,您可能希望结果数据绑定到返回的实体。Micronaut Data 不会自动这样做,而是需要指定相关的 @Join 注解。

例如:

    @Query("SELECT *, m_.name as m_name, m_.id as m_id FROM product p INNER JOIN manufacturer m_ ON p.manufacturer_id = m_.id WHERE p.name like :name limit 5")
@Join(value = "manufacturer", alias = "m_")
List<Product> searchProducts(String name);

在上例中,查询使用名为 m_ 的别名,通过 INNER JOIN 查询 manufacturer 表。由于返回的 "产品 "实体具有制造商关联,因此最好也将此对象实体化。@Join 注解中的 alias 成员用于指定从哪个别名实体化 Manufacturer 实例。

提示

有必要在 @Join 中使用字段的 "逻辑名称"(在 @Entity 类中使用的名称),而不是在本地查询中使用的名称。在前面的例子中,如果类中的名称是 myManufacturer,那么就需要使用 Join(value = "myManufacturer", alias = "m_"),而不需要修改本地 sql 查询中的任何内容

英文链接