spring-r2dbc について
リアクティブなデータアクセス層を構築する開発者向けのSpring Data R2DBCスキルガイドです。特にWebFluxを対象に、エンティティマッピング、リポジトリ、リアクティブトランザクション、接続プール、一般的な注意点を扱います。
主な機能
- 01R2DBC接続とプールの設定
- 02エンティティのマッピングとリポジトリ定義
- 03リアクティブトランザクションの利用
- 04バッチクエリによる関連データ処理
- 05StepVerifierによるリアクティブ処理のテスト
- 06接続やマッピングの問題の診断
活用シーン
- 01WebFlux向けリアクティブデータ層の構築
- 02データベースエンティティとリポジトリクエリの定義
- 03リアクティブトランザクションでの注文処理
- 04接続プールやエンティティマッピング障害の診断
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill spring-r2dbcインストール後、Claude Code で/spring-r2dbcを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
advanced.mddatabase-client.mdtransactions.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: ConnectionFactory | R2DBC ドライバーが不足している | 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.md | DatabaseClient、動的クエリ、集計 |
| transactions.md | トランザクション、リレーション、ページネーション |
| advanced.md | 監査、コンバーター、プール設定、テスト |
外部ドキュメント
コメント
Markdown 対応