Spring Boot 與 R2DBC 整合

lzyer發表於2021-08-14

R2DBC 是 "Reactive Relational Database Connectivity"的簡稱。
R2DBC 是一個 API 規範的倡議,宣告對於訪問關係型資料庫驅動實現了一些響應式的API。

R2DBC的誕生為了非阻塞的應用棧, 使用很少的執行緒可以處理大量的併發同時減少硬體資源。大量的應用還是使用的關係型資料庫,然而很多 NoSQL 資料提供了響應式客戶端,並不是所有的專案都適合遷移到 NoSQL。因此,R2DBC 應運而生。

R2DBC 特點

  • 對於 R2DBC 的驅動例項,Spring 支援基於Java 的@Connfiguration的配置
  • R2dbcEntityTemplate 作為實體繫結的核心操作類
  • 整合了Spring Conversion Service 豐富的物件對映
  • 基於註解後設資料對映關係
  • 自動實現 Reposity 介面,包含支援自定義的查詢方法

R2DBC 使用

接下來通過一個官方的例項來演示

依賴 pom.xml

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-r2dbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux</artifactId>
		</dependency>

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>io.r2dbc</groupId>
			<artifactId>r2dbc-h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>io.projectreactor</groupId>
			<artifactId>reactor-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>io.projectreactor</groupId>
			<artifactId>reactor-test</artifactId>
			<version>3.4.8</version>
			<scope>compile</scope>
		</dependency>
	</dependencies>

定義 Person 實體

public class Person {
  
    private final String id;
    private final String name;
    private final int age;

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

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person [id=" + id + ", name=" + name + ", age=" + age + "]";
    }
}

測試

import com.example.springreactivedemo.model.Person;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import reactor.test.StepVerifier;

public class Test1 {

    private static final Log log = LogFactory.getLog(Test1.class);

    public static void main(String[] args) {
        ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");


        R2dbcEntityTemplate template = new R2dbcEntityTemplate(connectionFactory);

        template.getDatabaseClient().sql("CREATE TABLE person" +
                        "(id VARCHAR(255) PRIMARY KEY," +
                        "name VARCHAR(255)," +
                        "age INT)")
                .fetch()
                .rowsUpdated()
                .as(StepVerifier::create)
                .expectNextCount(1)
                .verifyComplete();

        template.insert(Person.class)
                .using(new Person("joe", "Joe", 34))
                .as(StepVerifier::create)
                .expectNextCount(1)
                .verifyComplete();

        template.select(Person.class)
                .first()
                .doOnNext(it -> log.info(it))
                .as(StepVerifier::create)
                .expectNextCount(1)
                .verifyComplete();
    }
}

執行結果:

15:34:40.101 [main] DEBUG org.springframework.r2dbc.core.DefaultDatabaseClient - Executing SQL statement [INSERT INTO person (id, name, age) VALUES ($1, $2, $3)]
15:34:40.103 [main] DEBUG io.r2dbc.h2.client.SessionClient - Request:  INSERT INTO person (id, name, age) VALUES ($1, $2, $3) {1: 'joe', 2: 'Joe', 3: 34}
Exception in thread "main" java.lang.AssertionError: expectation "expectNextCount(1)" failed (expected: count = 1; actual: counted = 0; signal: onError(java.lang.IllegalStateException: Required identifier property not found for class com.example.springreactivedemo.model.Person!))
	at reactor.test.MessageFormatter.assertionError(MessageFormatter.java:115)
	at reactor.test.MessageFormatter.failPrefix(MessageFormatter.java:104)
	at reactor.test.MessageFormatter.fail(MessageFormatter.java:73)
	at reactor.test.MessageFormatter.failOptional(MessageFormatter.java:88)

通過執行官方文件的例項出現了以上的報錯資訊,我們來看一下錯誤資訊,Required identifier property not found for class com.example.springreactivedemo.model.Person! , Person 要求一個唯一屬性,將 Person 類 id 增加了註解@Id , 再次執行成功,日誌如下。

15:38:06.806 [main] DEBUG org.springframework.r2dbc.core.DefaultDatabaseClient - Executing SQL statement [INSERT INTO person (id, name, age) VALUES ($1, $2, $3)]
15:38:06.808 [main] DEBUG io.r2dbc.h2.client.SessionClient - Request:  INSERT INTO person (id, name, age) VALUES ($1, $2, $3) {1: 'joe', 2: 'Joe', 3: 34}
15:38:06.847 [main] DEBUG io.r2dbc.h2.client.SessionClient - Request:  CALL H2VERSION()
15:38:06.847 [main] DEBUG io.r2dbc.h2.client.SessionClient - Response: org.h2.result.LocalResultImpl@72c927f1 columns: 1 rows: 1 pos: -1
15:38:06.847 [main] DEBUG org.springframework.r2dbc.core.DefaultDatabaseClient - Executing SQL statement [SELECT person.* FROM person LIMIT 1]
15:38:06.853 [main] DEBUG io.r2dbc.h2.client.SessionClient - Request:  SELECT person.* FROM person LIMIT 1
15:38:06.854 [main] DEBUG io.r2dbc.h2.client.SessionClient - Response: org.h2.result.LocalResultImpl@1c32886a columns: 3 rows: 1 pos: -1
15:38:06.876 [main] INFO com.example.springreactivedemo.client.Test1 - Person [id=joe, name=Joe, age=34]

Spring Boot 整合 R2DBC 流程

1. 建立 ConnectionFactory 和 R2dbcEntityTemplate
@Configuration
public class ApplicationConfiguration extends AbstractR2dbcConfiguration {

    @Override
    @Bean
    public ConnectionFactory connectionFactory() {
        return new H2ConnectionFactory(H2ConnectionConfiguration.builder()
                .url("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE")
                .build());
    }

    @Bean
    public R2dbcEntityTemplate r2dbcEntityTemplate() {
        return new R2dbcEntityTemplate(connectionFactory());
    }
}
2. CRUD 使用
@RestController
@Slf4j
public class PersonController {

    private R2dbcEntityTemplate r2dbcEntityTemplate;

    public PersonController(R2dbcEntityTemplate r2dbcEntityTemplate) {
        this.r2dbcEntityTemplate = r2dbcEntityTemplate;
        // 建立表結構
        r2dbcEntityTemplate.getDatabaseClient().sql("drop table person if exists; CREATE TABLE person" +
                "(id VARCHAR(255) PRIMARY KEY," +
                "name VARCHAR(255)," +
                "age INT)").fetch().rowsUpdated().subscribe();
    }

    @PostMapping("/save")
    public Mono<Person> insert(@RequestBody Person person) {
        return r2dbcEntityTemplate.insert(Person.class).using(person);
    }

    @GetMapping("/list")
    public Flux<Person> list() {
        return r2dbcEntityTemplate.select(Query.empty(), Person.class);
    }

    @PutMapping("/update")
    public void update(@RequestBody Person person) {
        r2dbcEntityTemplate.update(Person.class)
                .inTable("person") //可以指定 table
                .matching(Query.query(Criteria.where("id").is(person.getId())))
                .apply(Update.update("name", person.getName())).subscribe();
    }

    @DeleteMapping("/delete")
    public Mono<Integer> delete(@RequestBody Person person) {
        return r2dbcEntityTemplate.delete(Person.class).matching(Query.query(Criteria.where("id").is(person.getId()))).all();
    }
}

總結

Spring Boot 整合 R2DBC 簡單例項使用完成,需要注意的是響應式程式設計和之前指令式程式設計有很大的區別,在寫update方法中,在 R2dbcEntityTemplate 執行 update 最後沒有呼叫 subscribe,導致 update 方法沒有執行,在響應式程式設計中定義的方法流程需要通過觸發才會執行。程式碼裡面使用 subscribe 的方式來觸發呼叫,還可以將執行 update返回的值 Mono<Integer>,這樣也會觸發執行。

參考:

https://docs.spring.io/spring-data/r2dbc/docs/current/reference/html/#introduction

相關文章