7.3 响应式 Repository

Spring Data R2DBC 响应式 Repository 模式:ReactiveCrudRepository 与 ReactiveSortingRepository、派生查询方法 naming convention、@Query 注解自定义 SQL、DatabaseClient 底层操作,以及与 JPA 的对比。

ReactiveCrudRepository

Spring Data R2DBC 的核心是 ReactiveCrudRepository:

import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;

public interface UserRepository extends ReactiveCrudRepository<User, Long> {
    Mono<User> findByName(String name);
    Flux<User> findByStatus(String status);
    Mono<Boolean> existsByName(String name);
}

Like 的 % 和 _ 不会被自动补上,findBySymbolLike("BTC") 等价于 symbol = 'BTC',想模糊匹配要传 "%BTC%"。这是 Like 与 Containing、StartingWith 的关键区别:后者由 Spring Data 负责加通配符。

Spring Data 会在运行时自动生成实现。方法命名遵循约定:

前缀作用示例
find...By查询findByName → WHERE name = ?
find...By...And...多条件findByStatusAndCreatedAtAfter
count...By计数countByStatus
exists...By存在判断existsByName
delete...By删除deleteByStatus

派生查询的关键字

public interface OrderRepository extends ReactiveCrudRepository<Order, Long> {
    // 等于
    Flux<Order> findBySymbol(String symbol);

    // 大于 / 小于
    Flux<Order> findByPriceGreaterThan(double price);
    Flux<Order> findByQuantityLessThanEqual(int qty);

    // Like:通配符要调用方自己带,这里传 "%BTC%"
    Flux<Order> findBySymbolLike(String pattern);

    // 区间
    Flux<Order> findByCreatedAtBetween(Instant start, Instant end);

    // In
    Flux<Order> findByStatusIn(List<String> statuses);

    // Ignore case
    Flux<Order> findBySymbolIgnoreCase(String symbol);

    // 排序
    Flux<Order> findByStatusOrderByCreatedAtDesc(String status);

    // 分页
    Flux<Order> findByStatus(String status, Pageable pageable);
    Mono<Long> countByStatus(String status);
}

@Query 自定义 SQL

当派生查询无法满足需求时,用 @Query:

public interface OrderRepository extends ReactiveCrudRepository<Order, Long> {

    @Query("""
        SELECT o.*, u.name as user_name
        FROM orders o
        JOIN users u ON o.user_id = u.id
        WHERE o.symbol = :symbol
        ORDER BY o.created_at DESC
        LIMIT :limit
        """)
    Flux<OrderWithUser> findOrdersWithUserBySymbol(
        @Param("symbol") String symbol,
        @Param("limit") int limit
    );

    @Query("SELECT COUNT(*) FROM orders WHERE created_at > :since")
    Mono<Long> countRecentOrders(@Param("since") Instant since);
}

:symbol 是命名参数(需要 @Param 注解或编译时 -parameters 标志)。@Query 的返回值可以是 Mono<T>、Flux<T> 或 Mono<Void>。

DatabaseClient:底层操作

DatabaseClient 是 Spring Data R2DBC 的低级 API,介于原始 R2DBC SPI 和 Repository 之间:

@Service
public class ReportService {
    private final DatabaseClient db;

    public ReportService(DatabaseClient db) {
        this.db = db;
    }

    public Mono<Map<String, Object>> getStats() {
        return db.sql("""
            SELECT
                COUNT(*) as total_orders,
                SUM(price * quantity) as total_value,
                AVG(price) as avg_price
            FROM orders
            WHERE status = :status
            """)
            .bind("status", "COMPLETED")
            .fetch()
            .one();  // 返回单行 Map
    }

    public Flux<Map<String, Object>> getDailyReport() {
        return db.sql("""
            SELECT DATE(created_at) as day, COUNT(*) as cnt
            FROM orders
            GROUP BY DATE(created_at)
            ORDER BY day DESC
            """)
            .fetch()
            .all();  // 返回多行 Flux
    }
}

DatabaseClient 适合报表、聚合、复杂 JOIN 等不适合 Repository 派生查询的场景。

实体映射

Spring Data R2DBC 需要显式表映射(不像 JPA 自动推断):

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;

@Table("users")
public record User(
    @Id Long id,
    String name,
    String email,
    String status
) {}
注解作用
@Table指定表名
@Id主键
@Column自定义列名(字段名不同时)
@Transient不持久化
@CreatedDate / @LastModifiedDate自动填充时间戳
@Table("orders")
public record Order(
    @Id Long id,
    @Column("user_id") Long userId,
    String symbol,
    int quantity,
    double price,
    @CreatedDate Instant createdAt
) {}

启用审计:

@Configuration
@EnableR2dbcAuditing
public class R2dbcConfig {
}

分页与排序

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;

// 分页:页码 0-based,index=1 是人类习惯的第 2 页;每页 20 条,按 createdAt 降序
PageRequest page = PageRequest.of(1, 20, Sort.by("createdAt").descending());

Flux<Order> pageContent = orderRepo.findByStatus("PENDING", page);
Mono<Long> total = orderRepo.countByStatus("PENDING");

注意:R2DBC Repository 的 Page<T> 返回不支持(没有 findAll(Pageable)),需要手动组合 Flux<T> + Mono<Long>。

与 JPA 的对比

能力JPAR2DBC
延迟加载(Lazy)✅ @ManyToOne(fetch = LAZY)❌ 无延迟加载
级联操作✅ CascadeType.ALL❌ 手动处理
一级缓存/脏检查✅ 持久化上下文内自动❌ 无状态,每次查询都走数据库
二级缓存✅ JPA 二级缓存❌ 无内置缓存,可用 Spring Cache(第 9 章)
自动 DDL✅ hibernate.ddl-auto⚠️ 需 Flyway/Liquibase
响应式❌ 阻塞✅ Mono/Flux
复杂查询JPQL / Criteria原生 SQL / @Query

R2DBC 是「更简单但更底层」的选择。如果你需要响应式,放弃 JPA 的自动魔法是必要代价。

eventd v2 的数据层

eventd-data 的 Repository 结构:

// UserRepository.java
public interface UserRepository extends ReactiveCrudRepository<User, Long> {
    Mono<User> findByName(String name);
}

// OrderRepository.java
public interface OrderRepository extends ReactiveCrudRepository<Order, Long> {
    @Query("SELECT * FROM orders WHERE user_id = :userId ORDER BY created_at DESC LIMIT :limit")
    Flux<Order> findRecentByUserId(@Param("userId") Long userId, @Param("limit") int limit);
}

// InventoryRepository.java(纯 DatabaseClient)
@Service
public class InventoryRepository {
    private final DatabaseClient db;
    public Mono<Integer> deduct(String symbol, int qty) {
        return db.sql("UPDATE inventory SET quantity = quantity - :qty WHERE symbol = :symbol AND quantity >= :qty")
            .bind("qty", qty).bind("symbol", symbol)
            .fetch().rowsUpdated();
    }
}

小结

层级API适用场景
高级ReactiveCrudRepositoryCRUD + 简单查询
中级@Query 注解自定义 SQL
低级DatabaseClient报表、聚合、复杂 JOIN
原始R2DBC SPI性能调优、特殊驱动功能

第 7 章结束。下一节 8.1 Schema 变更 :Flyway 配置与响应式兼容。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南