Backend

99 Ngày Spring — Ngày 83: R2DBC

SSite Admin
18 tháng 10, 2026 2 phút đọc 5 lượt xem
99 Ngày Spring — Ngày 83: R2DBC

WebFlux không chặn chỉ có ý nghĩa khi tầng dữ liệu cũng không chặn. JDBC là API blocking: mỗi truy vấn giữ một luồng cho tới khi database trả lời. R2DBC là đặc tả driver non-blocking cho database quan hệ, và Spring Data R2DBC đặt repository lên trên nó. Bài này chỉ ra bạn nhận được gì và mất gì so với JPA.

R2DBC

1. Cấu hình: starter, driver và URL

Thêm spring-boot-starter-data-r2dbc và driver của database, ví dụ org.postgresql:r2dbc-postgresql. URL bắt đầu bằng r2dbc: thay cho jdbc:. Boot tự cấu hình ConnectionFactory và connection pool. Lưu ý Flyway và Liquibase vẫn chạy migration qua JDBC, nên cần khai thêm một URL JDBC riêng cho chúng.

spring.r2dbc.url=r2dbc:postgresql://localhost:5432/shop
spring.r2dbc.username=shop
spring.r2dbc.password=${DB_PASSWORD}
spring.r2dbc.pool.max-size=20
# Migrations still use JDBC
spring.flyway.url=jdbc:postgresql://localhost:5432/shop
spring.flyway.user=shop
spring.flyway.password=${DB_PASSWORD}

2. Repository trả Mono và Flux

Entity là một lớp thường hoặc record với @Table và @Id của Spring Data, không phải annotation của JPA. Repository kế thừa ReactiveCrudRepository; phương thức truy vấn dẫn xuất và @Query vẫn có. Kết quả là Mono hoặc Flux, và các hàng được đẩy về dần theo nhu cầu của subscriber thay vì gom hết vào một List.

@Table("orders")
record Order(@Id Long id, String customerId, BigDecimal total, String status) {}

interface OrderRepository extends ReactiveCrudRepository<Order, Long> {
    Flux<Order> findByCustomerId(String customerId);

    @Query("select * from orders where status = :status limit :size")
    Flux<Order> findByStatus(String status, int size);
}

3. Transaction reactive

@Transactional vẫn dùng được trên phương thức trả Mono hoặc Flux; Boot cấu hình R2dbcTransactionManager. Khác biệt là transaction gắn với Reactor Context chứ không phải ThreadLocal, nên mọi thao tác phải nằm trong cùng một chuỗi. Gọi subscribe() riêng hoặc block() ở giữa là thoát khỏi transaction mà không có cảnh báo nào.

@Transactional
Mono<Order> place(NewOrder input) {
    return orders.save(input.toOrder())
        .flatMap(saved -> outbox.save(OutboxEvent.created(saved))
            .thenReturn(saved));
}

4. Đánh đổi so với JPA

Spring Data R2DBC cố ý đơn giản: nó là một object mapper, không phải ORM đầy đủ. Không có lazy loading, không có dirty checking, không có cache cấp một, và không ánh xạ quan hệ kiểu @OneToMany. Bạn tự viết join hoặc ghép dữ liệu bằng nhiều truy vấn. Nếu domain dựa nhiều vào đồ thị entity của JPA, chuyển sang R2DBC là viết lại tầng dữ liệu.

  • Chọn R2DBC khi service đã reactive từ đầu tới cuối và truy vấn tương đối phẳng.

  • Giữ JPA khi cần ORM đầy đủ; trên Java 21, MVC với virtual thread cũng chịu được nhiều request chờ database.

  • Đừng trộn: gọi JPA từ event loop là chặn event loop.

  • Pool kết nối vẫn là giới hạn thật; non-blocking không tạo thêm kết nối cho database.

R2DBC đổi sự tiện lợi của ORM lấy một tầng dữ liệu không chặn. Ngày 84 dùng nền reactive đó cho dữ liệu realtime: SSE và WebSocket.

Tài liệu đối chiếu

  • https://docs.spring.io/spring-data/relational/reference/r2dbc.html

  • https://r2dbc.io/

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Java — Ngày 83: Sealed Classes

sealed và permits, ba lựa chọn của lớp con, switch đầy đủ không cần default và cách mô hình hóa domain.

18 thg 10, 20263 phút4
99 Ngày Spring — Ngày 82: Reactive Operators và Backpressure

map, flatMap, concatMap, zip, xử lý lỗi bằng onErrorResume và retryWhen, cùng khái niệm backpressure trong Reactor.

17 thg 10, 20263 phút6
99 Ngày Java — Ngày 82: Switch Expression và Pattern Matching

Cú pháp mũi tên, yield, type pattern, guard when, record pattern và tính đầy đủ của switch trên Java 21.

17 thg 10, 20263 phút7