无法使用带有 Spring Data 反应式存储库的 Oracle R2DBC 驱动程序执行任何查询

Posted

技术标签:

【中文标题】无法使用带有 Spring Data 反应式存储库的 Oracle R2DBC 驱动程序执行任何查询【英文标题】:Unable to execute any query using Oracle R2DBC driver with Spring Data reactive repositories 【发布时间】:2021-10-10 09:42:33 【问题描述】:

我是第一次使用 SpringData Reactive Repositories。

我一直在研究official documentation,并创建了一个基本的 CRUD API 来使用它们。

为了简单起见,我从 H2 开始,一切都按预期工作。

当我尝试创建一个新实体时,一切正常:

% curl -v -# -X POST http://localhost:8080/wallet/
*   Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> POST /wallet/ HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.77.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 201 Created
< Content-Type: application/json
< Content-Length: 57
< 
* Connection #0 to host localhost left intact
"id":"6cccd902-01a4-4a81-8166-933b2a109ecc","balance":0

代码非常简单(通常使用 SpringData Repositories):

import com.jfcorugedo.reactivedemo.wallet.model.Wallet;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;

public interface WalletRepository extends ReactiveCrudRepository<Wallet, String> 

还有控制器:

import com.jfcorugedo.reactivedemo.wallet.dao.WalletRepository;
import com.jfcorugedo.reactivedemo.wallet.model.Wallet;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

import java.math.BigDecimal;

import static org.springframework.data.relational.core.query.Query.query;

@RestController
@RequestMapping("wallet")
@Slf4j
public class WalletController 

    private WalletRepository walletRepository;

    @Autowired
    private R2dbcEntityTemplate template;

    public WalletController(WalletRepository walletRepository) 
        this.walletRepository = walletRepository;
    

    @GetMapping("id")
    public Mono<ResponseEntity<Wallet>> get(@PathVariable("id") String id) 

        return walletRepository
                .findById(id)
                .map(ResponseEntity::ok)
                .defaultIfEmpty(ResponseEntity.notFound().build());
    

    @GetMapping("count")
    public Mono<ResponseEntity<Long>> count() 

        return walletRepository
                .count()
                .map(ResponseEntity::ok)
                .defaultIfEmpty(ResponseEntity.notFound().build());
    

    @PostMapping
    public Mono<ResponseEntity<Wallet>> create() 

        return walletRepository
                .save(Wallet.empty())
                .map(w -> ResponseEntity.status(201).body(w));
    

    @PostMapping("/entityTemplate")
    public Mono<ResponseEntity<Wallet>> insert() 

        log.info("Inserting using R2dbcEntityTemplate");
        return template.insert(new Wallet(null, BigDecimal.ZERO))
                .map(ResponseEntity::ok);
    

DTO 也很简单:

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.annotation.Id;

import java.math.BigDecimal;

@AllArgsConstructor
@Getter
public class Wallet 

    @Id
    private String id;
    private BigDecimal balance;

    public static Wallet empty() 
        return new Wallet(null, BigDecimal.ZERO);
    

    public Wallet withId(String id) 
        return new Wallet(id, this.balance);
    

然后我查看Oracle is also supported的文档。

我又翻了官方Oracle driver documentation。

这个驱动程序确实正在开发中,所以它还没有准备好生产。

但是,我克隆了存储库并尝试在本地 Oracle 实例上执行一些测试,一切正常。

这是我直接使用Oracle驱动执行的代码:

String r2dbcUrl = "r2dbc:oracle://?oracleNetDescriptor="+DESCRIPTOR;
    Mono.from(ConnectionFactories.get(ConnectionFactoryOptions.parse(r2dbcUrl)
      .mutate()
      .option(ConnectionFactoryOptions.USER, USER)
      .option(ConnectionFactoryOptions.PASSWORD, PASSWORD)
      .build())
      .create())
      .flatMapMany(connection ->
        Mono.from(connection.createStatement(
          "INSERT INTO WALLET (ID, BALANCE) VALUES ('" + UUID.randomUUID().toString() + "', 0)")
          .execute())
          .flatMapMany(result ->
            result.map((row, metadata) -> row.get(0, String.class)))
          .concatWith(Mono.from(connection.close()).cast(String.class)))
      .toStream()
      .forEach(System.out::println);

    // A descriptor may also be specified as an Option
    Mono.from(ConnectionFactories.get(ConnectionFactoryOptions.builder()
      .option(ConnectionFactoryOptions.DRIVER, "oracle")
      .option(Option.valueOf("oracleNetDescriptor"), DESCRIPTOR)
      .option(ConnectionFactoryOptions.USER, USER)
      .option(ConnectionFactoryOptions.PASSWORD, PASSWORD)
      .build())
      .create())
      .flatMapMany(connection ->
        Mono.from(connection.createStatement(
          "SELECT * from wallet")
          .execute())
          .flatMapMany(result ->
            result.map((row, metadata) -> row.get(0, String.class)))
          .concatWith(Mono.from(connection.close()).cast(String.class)))
      .toStream()
      .forEach(System.out::println);

我正在使用 Oracle 开发人员在示例文件夹中提供的代码。

执行此代码后,一切正常,并在我的 WALLET 表中创建了一个新行。

最后我尝试在 SpringData 中做同样的事情。

我使用完全相同的 DESCRIPTOR、USER 和 PASSWORD 连接到 Oracle。

这是我用来获取 ConnectionFactory 的配置类:

package com.jfcorugedo.reactivedemo.config;

import com.jfcorugedo.reactivedemo.wallet.model.Wallet;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryOptions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
import org.springframework.data.r2dbc.mapping.event.BeforeConvertCallback;

import reactor.core.publisher.Mono;

import java.util.UUID;

@Configuration
@ConditionalOnProperty(name = "dababase.vendor", havingValue = "oracle")
@Slf4j
public class OracleR2dbcConfig extends AbstractR2dbcConfiguration 

    @Value("$database.host:localhost")
    private String host;

    @Value("$database.port:1521")
    private int port;

    @Value("$database.serviceName")
    private String serviceName;

    @Override
    @Bean("r2dbcConnectionFactory")
    public ConnectionFactory connectionFactory() 
        String descriptor = "(DESCRIPTION=" +
                "(ADDRESS=(HOST=" + host + ")(PORT=" + port + ")(PROTOCOL=tcp))" +
                "(CONNECT_DATA=(SERVICE_NAME=" + serviceName + ")))";

        log.info("Creating connection factory with descriptor " + descriptor);

        String r2dbcUrl = "r2dbc:oracle://?oracleNetDescriptor="+descriptor;
        return ConnectionFactories.get(ConnectionFactoryOptions.parse(r2dbcUrl)
                .mutate()
                .option(ConnectionFactoryOptions.USER, "jfcorugedo")
                .option(ConnectionFactoryOptions.PASSWORD, System.getenv("DB_PASSWORD"))
                .build());
    

    @Bean
    BeforeConvertCallback<Wallet> idGenerator() 
        return (entity, table) -> entity.getId() == null ? Mono.just(entity.withId(UUID.randomUUID().toString())) : Mono.just(entity);
    

它与我在另一个项目中使用的非常相似:

private static final String DESCRIPTOR = "(DESCRIPTION=" +
    "(ADDRESS=(HOST="+HOST+")(PORT="+PORT+")(PROTOCOL=tcp))" +
    "(CONNECT_DATA=(SERVICE_NAME="+SERVICE_NAME+")))";
...
String r2dbcUrl = "r2dbc:oracle://?oracleNetDescriptor="+DESCRIPTOR;
    Mono.from(ConnectionFactories.get(ConnectionFactoryOptions.parse(r2dbcUrl)
      .mutate()
      .option(ConnectionFactoryOptions.USER, USER)
      .option(ConnectionFactoryOptions.PASSWORD, PASSWORD)
      .build())
      .create())
...

切换到Oracle后,SpringBoot应用启动没有任何错误:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.3)

2021-08-05 12:48:22.891  INFO 99453 --- [           main] c.j.r.ReactiveDemoApplication            : Starting ReactiveDemoApplication using Java 11.0.10 on APM3LC02CH2VNMD6R with PID 99453 (/Users/lp68ba/Developer/personal/reactive-demo/target/classes started by lp68ba in /Users/lp68ba/Developer/personal/reactive-demo)
2021-08-05 12:48:22.892  INFO 99453 --- [           main] c.j.r.ReactiveDemoApplication            : No active profile set, falling back to default profiles: default
2021-08-05 12:48:23.165  INFO 99453 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data R2DBC repositories in DEFAULT mode.
2021-08-05 12:48:23.214  INFO 99453 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 45 ms. Found 1 R2DBC repository interfaces.
2021-08-05 12:48:23.554  INFO 99453 --- [           main] c.j.r.config.OracleR2dbcConfig           : Creating connection factory with descriptor (DESCRIPTION=(ADDRESS=(HOST=localhost)(PORT=1521)(PROTOCOL=tcp))(CONNECT_DATA=(SERVICE_NAME=ORCLCDB)))
2021-08-05 12:48:24.129  INFO 99453 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port 8080
2021-08-05 12:48:24.142  INFO 99453 --- [           main] c.j.r.ReactiveDemoApplication            : Started ReactiveDemoApplication in 1.466 seconds (JVM running for 2.021)

但是现在当我尝试执行任何操作时,连接保持打开状态并且没有任何反应:

% curl -v -# -X POST http://localhost:8080/wallet/
*   Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> POST /wallet/ HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.77.0
> Accept: */*
> 

在应用程序的日志中,我可以看到以下跟踪:

2021-08-05 13:08:20.735 DEBUG 144 --- [nPool-worker-19] o.s.r2dbc.core.DefaultDatabaseClient     : Executing SQL statement [INSERT INTO WALLET (ID, BALANCE) VALUES (:P0_id, :P1_balance)]

但是执行永远不会结束,并且不会在数据库中创建任何内容。

我都尝试过:Spring Data Reactive Repositories 和 R2DBCEntityTemplate,结果相同。

我已经生成了带有一些痕迹的 Oracle R2DBC 驱动程序的自定义版本,这就是我所拥有的:

直接使用 Oracle R2DBC 驱动程序(一切正常):

Creating OracleConnectionFactoryImpl with options: ConnectionFactoryOptionsoptions=driver=oracle, oracleNetDescriptor=(DESCRIPTION=(ADDRESS=(HOST=localhost)(PORT=1521)(PROTOCOL=tcp))(CONNECT_DATA=(SERVICE_NAME=ORCLCDB))), password=REDACTED, user=jfcorugedo
Oracel reactive adapter obtained: oracle.r2dbc.impl.OracleReactiveJdbcAdapter@c33b74f
Datasource obtained: oracle.jdbc.pool.OracleDataSource@696da30b
Creating a new connection
using adatper y datasource to create a new connection
Creating a OracleConnectionImpl with JDBC connection oracle.jdbc.driver.T4CConnection@10f7f7de
createStatement(sql): INSERT INTO WALLET (ID, BALANCE) VALUES ('9a3ab3db-ec38-4544-ac87-4e1a4ad40343', 0)
close()

使用 SpringData Reactive Repositories(连接卡住,没有任何反应):

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.3)

2021-08-05 13:12:49.557  INFO 304 --- [           main] c.j.r.ReactiveDemoApplication            : Starting ReactiveDemoApplication using Java 11.0.10 on APM3LC02CH2VNMD6R with PID 304 (/Users/lp68ba/Developer/personal/reactive-demo/target/classes started by lp68ba in /Users/lp68ba/Developer/personal/reactive-demo)
2021-08-05 13:12:49.559  INFO 304 --- [           main] c.j.r.ReactiveDemoApplication            : No active profile set, falling back to default profiles: default
2021-08-05 13:12:49.849  INFO 304 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data R2DBC repositories in DEFAULT mode.
2021-08-05 13:12:49.891  INFO 304 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 38 ms. Found 1 R2DBC repository interfaces.
2021-08-05 13:12:50.208  INFO 304 --- [           main] c.j.r.config.OracleR2dbcConfig           : Creating connection factory with descriptor (DESCRIPTION=(ADDRESS=(HOST=localhost)(PORT=1521)(PROTOCOL=tcp))(CONNECT_DATA=(SERVICE_NAME=ORCLCDB)))
Creating OracleConnectionFactoryImpl with options: ConnectionFactoryOptionsoptions=driver=oracle, oracleNetDescriptor=(DESCRIPTION=(ADDRESS=(HOST=localhost)(PORT=1521)(PROTOCOL=tcp))(CONNECT_DATA=(SERVICE_NAME=ORCLCDB))), password=REDACTED, user=jfcorugedo
Oracel reactive adapter obtained: oracle.r2dbc.impl.OracleReactiveJdbcAdapter@5f172d4a
Datasource obtained: oracle.jdbc.pool.OracleDataSource@934b52f
2021-08-05 13:12:50.736  INFO 304 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port 8080
2021-08-05 13:12:50.745  INFO 304 --- [           main] c.j.r.ReactiveDemoApplication            : Started ReactiveDemoApplication in 1.417 seconds (JVM running for 4.428)
Creating a new connection
using adatper y datasource to create a new connection
Creating a OracleConnectionImpl with JDBC connection oracle.jdbc.driver.T4CConnection@42dce884
2021-08-05 13:12:54.481 DEBUG 304 --- [nPool-worker-19] o.s.r2dbc.core.DefaultDatabaseClient     : Executing SQL statement [INSERT INTO WALLET (ID, BALANCE) VALUES (:P0_id, :P1_balance)]
OracleConnectionImpl#createStatement(sql): INSERT INTO WALLET (ID, BALANCE) VALUES (:P0_id, :P1_balance)
Creating OracleStatementImpl with SQL: INSERT INTO WALLET (ID, BALANCE) VALUES (:P0_id, :P1_balance)
OracleConnectionImpl#close()

我不知道为什么执行会被 SpringData 卡住。连接似乎没问题,我在这里使用的参数与直接用于 Oracle 驱动程序的参数完全相同。

有人有使用 SpringData R2DBC 存储库和 Oracle R2DBC 驱动程序的工作示例吗?

您可以查看repository中的代码。

【问题讨论】:

请发布您的控制器代码等。您的“其他代码”不是那么相关。相关的是执行的代码。例如,您为什么打电话给repo.save(Mono.empty()) 在此期间会发生什么?这对我来说有点不清楚。 完成!无论如何,您都有 Github 存储库的 URL。关于保存操作,请注意:不是Mono.empty()而是Wallet.empty(),我的DTO中生成空钱包的方法 如果不将其包装在 ResponseEntity 中会怎样?可以使用@ResponseStatus注解来设置方法的HTTP状态码。 我可以测试,但我认为控制器没有任何问题。它适用于任何其他数据库 感谢@jfcorugedo 报告此事。我应该有时间在第二天左右调试它。看起来 Oracle R2DBC 正在做其他驱动程序没有做的事情。我会想纠正那个。 【参考方案1】:

目前,在使用 Spring Data 进行编程时,请坚持使用 0.1.0 版的 Oracle R2DBC。

较新版本的 Oracle R2DBC 实现了 R2DBC SPI 的 0.9.0.M1 版本,Spring Data 目前不支持该版本。这在 GitHub 讨论中得到了证实: https://github.com/oracle/oracle-r2dbc/issues/30#issuecomment-862989986

一旦我回滚到 Oracle R2DBC 的 0.1.0 版,我就能够让演示应用程序正常工作。 我不得不重构 OracleR2dbcConfig.java,因为直到 0.1.0 之后才添加对 Oracle 网络描述符的支持。一个普通的 URL 可以很好地配置主机、端口和服务名称:

    public ConnectionFactory connectionFactory() 
        String url =
          String.format("r2dbc:oracle://%s:%d/%s", host, port, serviceName);
        log.info("Creating connection factory with URL:" + url);

        return ConnectionFactories.get(ConnectionFactoryOptions.parse(url)
                .mutate()
                .option(ConnectionFactoryOptions.USER, user)
                .option(ConnectionFactoryOptions.PASSWORD, System.getenv("DB_PASSWORD"))
                .build());
    

另外,我必须在使用 curl 执行测试之前手动创建表:

create table wallet (id VARCHAR2(256), balance NUMBER);

我认为 Spring Data 通常会自动创建表,所以我不确定为什么必须手动执行此操作。如果未创建表,则 INSERT 将失败,并显示错误指示钱包表不存在:

ORA-04043: object WALLET does not exist

在进行这些更改后,curl 命令似乎正常运行:

curl -v -# -X POST http://localhost:8080/wallet/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /wallet/ HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 201 Created
< Content-Type: application/json
< Content-Length: 57
< 
* Connection #0 to host localhost left intact
"id":"2bcecf46-05eb-46b4-90ec-cfacff2bbaa8","balance":0* Closing connection 0

【讨论】:

谢谢迈克尔!不用担心创建表。我也必须这样做,除非您使用具有正确配置的 JPA,否则 Spring Data 不会创建表。

以上是关于无法使用带有 Spring Data 反应式存储库的 Oracle R2DBC 驱动程序执行任何查询的主要内容,如果未能解决你的问题,请参考以下文章

Spring存储库和带有nativeQuery = false的DATA_FORMAT

使用 Spring Data 创建只读存储库

spring-data-jpa 1.11.16 带游标的存储过程

spring-data 存储库自定义查询

Spring - 找到多个 Spring Data 模块,进入严格的存储库配置模式

在 spring data jpa 存储库中搜索许多可选参数