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

Unable to execute any query using Oracle R2DBC driver with Spring Data reactive repositories

我是第一次使用 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 开发人员在示例文件夹中提供的代码。

执行此代码后,一切正常,并且在我的钱包中创建了一个新行 table。

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

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

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

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 后,Spring启动应用程序没有任何错误:

  .   ____          _            __ _ _
 /\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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: ConnectionFactoryOptions{options={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: ConnectionFactoryOptions{options={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 Driver 时使用的参数完全相同。

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

您可以查看此repository中的代码。

目前,使用 Spring 数据进行编程时,坚持使用 Oracle R2DBC 0.1.0 版。

较新版本的 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 执行测试之前,我必须手动创建 table:

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

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

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