1. 前言

使用R2DBC操作MySQL數據庫 一文中初步介紹了 r2dbc-mysql 的使用。由於藉助 DatabaseClient 操作 MySQL ,過於初級和底層,不利於開發。今天就利用 Spring Data R2DBC 來演示 Spring 數據存儲抽象(Spring Data Repository) 風格的 R2DBC 數據庫操作。

請注意 :目前 Spring Data R2DBC 雖然已經迭代了多個正式版,但是仍然處於初級階段,還不足以運用到生產中。不過未來可期,值得研究學習。

2. Spring Data R2DBC

Spring Data R2DBC提供了基於 R2DBC 反應式關係數據庫驅動程序的流行的 Repository 抽象。但是這並不是一個ORM框架,你可以把它看做一個數據庫訪問的抽象層或者 R2DBC 的客戶端程序。它不提供 ORM 框架具有的緩存、懶加載等諸多特性,但它抽象了數據庫和對象的抽象映射關係,具有輕量級、易用性的特點。

2.1 版本對應關係

胖哥總結了截至目前 Spring Data R2DBCSpring Framework 的版本對應關係:

Spring Data R2DBC Spring Framework
1.0.0.RELEASE 5.2.2.RELEASE
1.1.0.RELEASE 5.2.6.RELEASE
1.1.1.RELEASE 5.2.7.RELEASE
1.1.2.RELEASE 5.2.8.RELEASE

一定要注意版本對應關係,避免不兼容的情況。

3. 基礎依賴

上次我沒有引用 R2DBC 連接池,這次我將嘗試使用它。主要依賴如下 ,這裏我還集成了 Spring Webflux :

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<!--  r2dbc 連接池 -->
<dependency>
    <groupId>io.r2dbc</groupId>
    <artifactId>r2dbc-pool</artifactId>
</dependency>
<!--r2dbc mysql 庫-->
<dependency>
    <groupId>dev.miku</groupId>
    <artifactId>r2dbc-mysql</artifactId>
</dependency>
<!--自動配置需要引入的一個嵌入式數據庫類型對象-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<!-- 反應式web框架 webflux-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

這裏我採用的是 Spring Boot 2.3.2.RELEASE

4. 配置

上次我們採用的是 JavaConfig 風格的配置,只需要向 Spring IoC 注入一個 ConnectionFactory 。這一次我將嘗試在 application.yaml 中配置 R2DBC 的必要參數。

spring:
  r2dbc:
    url: r2dbcs:mysql://127.0.0.1:3306/r2dbc
    username: root
    password: 123456

以上就是 R2DBC 的主要配置。特別注意的是 spring.r2dbc.url 的格式,根據數據庫的不同寫法是不同的,要看驅動的定義,這一點非常重要。連接池這裏使用默認配置即可,不用顯式定義。

5. 編寫業務代碼

接下來就是編寫業務代碼了。這裏我還嘗試使用 DatabaseClient 來執行了 DDL 語句創建了 client_user 表,感覺還不錯。

@Autowired
DatabaseClient databaseClient;

@Test
void doDDL() {

    List<String> ddl = Collections.unmodifiableList(Arrays.asList("drop table if exists client_user;", "create table client_user(user_id varchar(64) not null primary key,nick_name varchar(32),phone_number varchar(16),gender tinyint default 0) charset = utf8mb4;"));
    ddl.forEach(sql -> databaseClient.execute(sql)
            .fetch()
            .rowsUpdated()
            .as(StepVerifier::create)
            .expectNextCount(1)
            .verifyComplete());
}

5.1 聲明數據庫實體

熟悉 Spring Data JPA 的同學應該很輕車熟路了。

/**
 *  the client user type
 *
 * @author felord.cn
 */
@Data
@Table
public class ClientUser implements Serializable {
    private static final long serialVersionUID = -558043294043707772L;
    @Id
    private String userId;
    private String nickName;
    private String phoneNumber;
    private Integer gender;
}

5.2 聲明CRUD接口

上面實體類中的 @Table 註解是有說法的,當我們的操作接口繼承的是 ReactiveCrudRepository<T, ID> 或者 ReactiveSortingRepository<T, ID> 時,需要在實體類上使用 @Table 註解,這也是推薦的用法。

public interface ReactiveClientUserSortingRepository extends ReactiveSortingRepository<ClientUser,String> {
    
}

當然實體類不使用 @Table 註解標記時,我們還可以繼承 R2dbcRepository<T, ID> 接口。然後 ReactiveClientUserSortingRepository 將提供一些操作數據庫的方法。

然後 Spring Data JPA 怎麼寫,這裏也差不多怎麼寫,但是有些功能現在還沒有得到支持,比如上面提到的分頁,還有主鍵策略等。

類似 PagingAndSortingRepository<T,ID> 的反應式分頁功能接口目前還沒有實裝,會在未來的版本集成進來。

5.3 實際操作

接下來我們就要通過 R2DBC 實際操作 MySQL 數據庫了。按照我們傳統的邏輯寫了如下的新增邏輯:

ClientUser clientUser = new ClientUser();

clientUser.setGender(2);
clientUser.setNickName("r2dbc");
clientUser.setPhoneNumber("9527");
clientUser.setUserId("snowflake");

Mono<ClientUser> save = reactiveClientUserSortingRepository.save(clientUser);

結果數據庫並沒有寫入數據。這時因爲 r2dbc-mysql 不能被直接使用,只能由客戶端去實現並委託給客戶端去操作。

這也是 R2DBC 的設計原則,R2DBC的目標是最小化SPI平面,目的是消除數據庫之間的差異部分,並使得整個數據庫完全具有反應式和背壓。它主要用作客戶端庫使用的驅動程序SPI,而不打算直接在應用程序代碼中使用。

所以這裏我們可以藉助於 reactor-test 測試庫去執行一下,改寫爲:

reactiveClientUserSortingRepository.save(clientUser)
        .log()
        .as(StepVerifier::create)
        .expectNextCount(1)
        .verifyComplete();

但是依然不能執行成功,提示 update table [client_user]. Row with Id [snowflake] does not exist ,也就是說期望執行的是新增但是實際執行的是更新,由於數據庫找不到主鍵爲 snowflake 的記錄就報了錯。這裏爲什麼是更新呢?

這時因爲實體類在進行新增時會判斷主鍵是否填充,如果沒有填充就認爲是新數據,採取真正的新增操作,主鍵需要數據庫來自動填充;如果主鍵存在值則認爲是舊數據則調用更新操作。胖哥同 Spring Data R2DBC 的項目組溝通後並沒有得到友好的解決方案,不過我已經找到了方法,這裏先留個坑。

那麼該如何新增一條數據呢?我們只能藉助於 @Query 註解來編寫一條 SQL 寫入了:

@Modifying
@Query("insert into client_user (user_id,nick_name,phone_number,gender) values (:userId,:nickName,:phoneNumber,:gender)")
Mono<Integer> addClientUser(String userId, String nickName, String phoneNumber, Integer gender);

當添加了 @Modifying 後,返回值可以從 Mono<ClientUser>Mono<Boolean> 或者 Mono<Integer> 任意一種選擇。

reactiveClientUserSortingRepository
        .addClientUser("snowflake",
                "r2dbc",
                "132****155",
                0)
        .as(StepVerifier::create)
        .expectNextCount(1)
        .verifyComplete();

這樣就證明寫成功了一條數據。

5.4 搭配Webflux使用

但是實際中該如何應用呢?目前能夠想到的就是結合反應式框架 Spring Webflux 了,就像 Spring Data JPA 配合 Spring MVC 一樣。

我們編寫一個 Webflux 接口:

@RestController
@RequestMapping("/user")
public class ReactiveClientUserController {

    @Autowired
    private ReactiveClientUserSortingRepository reactiveClientUserSortingRepository;


    /**
     * 這裏爲了檢驗默認api 就不分層了
     *
     * @param userId the user id
     * @return the mono
     */
    @GetMapping("/{userId}")
    public Mono<ClientUser> findUserById(@PathVariable String userId) {
        return reactiveClientUserSortingRepository.findById(userId);
    }

}

5.5 一些測試數據參考

在低併發時, Spring MVC + JDBC 表現最佳,但在高併發下, WebFlux + R2DBC 使用每個已處理請求的內存最少。

在高併發下, Spring MVC + JDBC 的響應時間開始下降。顯然, R2DBC 在更高的併發性下提供了更好的響應時間。 Spring WebFlux 也比使用 Spring MVC 的類似實現更好。

6. 總結

今天對 Spring Data R2DBC 進一步演示,相信你能夠從中學到一些東西。由於 R2DBC 還是比較新,還存在一些需要改進和補充的東西。目前社區非常活躍,發展十分迅速。好了今天的文章就到這裏,原創不易多多關注: 碼農小胖哥 如果你覺得本文很有用,請點贊、轉發、再看。

關注公衆號:Felordcn 獲取更多資訊

個人博客:https://felord.cn

相關文章