无法使用带有 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-jpa 1.11.16 带游标的存储过程