mcpField
S
spring-r2dbc

spring-r2dbc

github.com/claude-dev-suite開発4641 回インストール2 日前に更新
リポジトリ

spring-r2dbc について

リアクティブなデータアクセス層を構築する開発者向けのSpring Data R2DBCスキルガイドです。特にWebFluxを対象に、エンティティマッピング、リポジトリ、リアクティブトランザクション、接続プール、一般的な注意点を扱います。

主な機能

  1. 01R2DBC接続とプールの設定
  2. 02エンティティのマッピングとリポジトリ定義
  3. 03リアクティブトランザクションの利用
  4. 04バッチクエリによる関連データ処理
  5. 05StepVerifierによるリアクティブ処理のテスト
  6. 06接続やマッピングの問題の診断

活用シーン

  1. 01WebFlux向けリアクティブデータ層の構築
  2. 02データベースエンティティとリポジトリクエリの定義
  3. 03リアクティブトランザクションでの注文処理
  4. 04接続プールやエンティティマッピング障害の診断

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill spring-r2dbc

インストール後、Claude Code で/spring-r2dbcを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

advanced.md
database-client.md
transactions.md

詳細

Spring Data R2DBC

クイックスタート


<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-r2dbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>r2dbc-postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>
# application.yml
spring:
  r2dbc:
    url: r2dbc:postgresql://localhost:5432/mydb
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    pool:
      enabled: true
      initial-size: 5
      max-size: 20

エンティティの定義

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

@Table("products")
public class Product {

    @Id
    private Long id;

    @Column("product_name")
    private String name;

    private String description;
    private BigDecimal price;

    @Column("category_id")
    private Long categoryId;

    @CreatedDate
    private Instant createdAt;

    @LastModifiedDate
    private Instant updatedAt;

    private boolean active;

    // Constructors, getters, setters...
}
// For immutable entities with records (Java 17+)
@Table("orders")
public record Order(
    @Id Long id,
    @Column("customer_id") Long customerId,
    BigDecimal total,
    OrderStatus status,
    @CreatedDate Instant createdAt
) {
    public Order withStatus(OrderStatus newStatus) {
        return new Order(id, customerId, total, newStatus, createdAt);
    }
}

リポジトリインターフェース

import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.data.r2dbc.repository.Query;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public interface ProductRepository extends R2dbcRepository<Product, Long> {

    // Automatic derived queries
    Flux<Product> findByActiveTrue();
    Flux<Product> findByNameContainingIgnoreCase(String name);
    Flux<Product> findByCategoryId(Long categoryId);
    Flux<Product> findByPriceBetween(BigDecimal min, BigDecimal max);
    Mono<Product> findByNameIgnoreCase(String name);

    // Ordering and limiting
    Flux<Product> findTop10ByActiveTrueOrderByCreatedAtDesc();

    // Count and Exists
    Mono<Long> countByActiveTrue();
    Mono<Boolean> existsByName(String name);

    // Custom query
    @Query("SELECT * FROM products WHERE category_id = :categoryId AND price < :maxPrice")
    Flux<Product> findByCategoryWithMaxPrice(Long categoryId, BigDecimal maxPrice);

    @Query("UPDATE products SET active = false WHERE id = :id")
    @Modifying
    Mono<Integer> deactivateProduct(Long id);

    // Projection with DTO
    @Query("SELECT id, product_name as name, price FROM products WHERE active = true")
    Flux<ProductSummary> findAllSummaries();
}

public record ProductSummary(Long id, String name, BigDecimal price) {}

全リファレンス:DatabaseClient を使った複雑なクエリについては、database-client.md を参照してください。


サービスレイヤー

@Service
@RequiredArgsConstructor
@Slf4j
public class ProductService {

    private final ProductRepository productRepository;

    // Create
    public Mono<Product> createProduct(CreateProductRequest request) {
        Product product = Product.create(request.name(), request.description(), request.price());
        product.setCategoryId(request.categoryId());
        return productRepository.save(product)
            .doOnSuccess(p -> log.info("Created product: {}", p.getId()));
    }

    // Read
    public Mono<Product> getProduct(Long id) {
        return productRepository.findById(id)
            .switchIfEmpty(Mono.error(new ProductNotFoundException(id)));
    }

    public Flux<Product> getAllActiveProducts() {
        return productRepository.findByActiveTrue();
    }

    // Update
    public Mono<Product> updateProduct(Long id, UpdateProductRequest request) {
        return productRepository.findById(id)
            .switchIfEmpty(Mono.error(new ProductNotFoundException(id)))
            .map(product -> {
                if (request.name() != null) product.setName(request.name());
                if (request.price() != null) product.setPrice(request.price());
                return product;
            })
            .flatMap(productRepository::save);
    }

    // Delete (soft delete)
    public Mono<Void> deactivateProduct(Long id) {
        return productRepository.deactivateProduct(id)
            .filter(count -> count > 0)
            .switchIfEmpty(Mono.error(new ProductNotFoundException(id)))
            .then();
    }
}

リアクティブトランザクション

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final OrderItemRepository orderItemRepository;

    @Transactional
    public Mono<Order> createOrder(CreateOrderRequest request) {
        return validateProducts(request.items())
            .then(calculateTotal(request.items()))
            .flatMap(total -> {
                Order order = new Order(null, request.customerId(), total, OrderStatus.PENDING, null);
                return orderRepository.save(order);
            })
            .flatMap(order -> saveOrderItems(order.id(), request.items())
                .then(Mono.just(order)));
    }

    private Mono<BigDecimal> calculateTotal(List<OrderItemRequest> items) {
        return Flux.fromIterable(items)
            .flatMap(item -> productRepository.findById(item.productId())
                .map(p -> p.getPrice().multiply(BigDecimal.valueOf(item.quantity()))))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

全リファレンス:TransactionalOperator とリレーションの処理については、transactions.md を参照してください。


ベストプラクティス

すべきことしてはいけないこと
WebFlux アプリケーションでは R2DBC を使うJDBC と R2DBC を混在させる
コネクションプールを設定するプールなしで使用する
バッチクエリでリレーションを手動で処理するJPA のような遅延読み込みを期待する
@Transactional または TransactionalOperator を使うトランザクション管理を忘れる
テストには StepVerifier を使う本番環境で .block() を使う

このスキルを使用すべきでない場合

  • ブロッキングアプリケーション - spring-data-jdbc または spring-data-jpa を使ってください
  • 複雑な ORM 機能 - R2DBC はシンプルです。遅延読み込みには JPA を使ってください
  • WebFlux を使用していない場合 - R2DBC はリアクティブスタック向けです
  • MongoDB のリアクティブ処理 - spring-data-mongodb のリアクティブリポジトリを使ってください

よくある落とし穴

エラー原因解決策
NoSuchBeanDefinitionException: ConnectionFactoryR2DBC ドライバーが不足しているr2dbc-postgresql/mysql の依存関係を追加する
Connection timeoutプールが枯渇しているmax-size を増やし、コネクションリークを確認する
TransactionRequiredException@Transactional が不足しているアノテーションを追加するか、TransactionalOperator を使う
エンティティがマッピングされないアノテーションが不足している@Table、@Id、@Column を確認する
N+1 クエリリレーションの読み込みIN 句を使ったバッチクエリを使う

アンチパターン

アンチパターン問題解決策
本番環境で .block() を使うイベントループをブロックするリアクティブオペレーターを使う
リレーションで N+1 クエリが発生するパフォーマンスの問題IN を使ったバッチクエリを使う
コネクションプールがないコネクションの枯渇r2dbc-pool を設定する
長時間のトランザクションコネクションが長時間保持されるトランザクションを短く保つ
エラーハンドリングがないエラーが見過ごされるonErrorResume、onErrorMap を使う

すぐにできるトラブルシューティング

問題診断修正
コネクションタイムアウトプール設定を確認するmax-size を増やし、リークを確認する
エンティティがマッピングされないアノテーションを確認する@Table、@Id、@Column を追加する
トランザクションが機能しない@Transactional を確認する必要に応じて TransactionalOperator を使う
クエリの結果が空カラム名を確認するマッピングが DB と一致していることを確認する
プールが枯渇しているアクティブなコネクションを監視するプールを増やし、リークを修正する

リファレンスファイル

ファイル内容
database-client.mdDatabaseClient、動的クエリ、集計
transactions.mdトランザクション、リレーション、ページネーション
advanced.md監査、コンバーター、プール設定、テスト

外部ドキュメント

コメント