mcpField
S
spring-boot-crud-patterns

spring-boot-crud-patterns

github.com/giuseppe-trisciuoglio開発3653.9k 回インストール7 週間前に更新
リポジトリ

spring-boot-crud-patterns について

Spring Boot 3.5+ の CRUD サービスを構築・診断するスキルです。機能単位の DDD に着想を得たレイヤー構成に沿い、ドメインモデル、JPA 永続化、アプリケーションサービス、DTO、REST コントローラーの実装と検証を支援します。

主な機能

  1. 01機能単位のパッケージ構成
  2. 02ドメイン集約とリポジトリポート
  3. 03JPA 永続化アダプター
  4. 04トランザクション対応のアプリケーションサービス
  5. 05検証付き DTO と REST コントローラー
  6. 06テストとマイグレーションの確認

活用シーン

  1. 01Spring Data JPA の CRUD エンドポイント作成
  2. 02ドメイン機能に沿ったサービス構成
  3. 03リポジトリやトランザクションの回帰診断
  4. 04REST 一覧エンドポイントへのページネーション追加

インストール

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

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill spring-boot-crud-patterns

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

同梱ファイル

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

assets/specs/
product_with_rel.json
product.json
references/
crud-reference.md
examples-product-feature.md
generator-usage.md
spring-official-docs.md
references/templates/
Controller.java.tpl
CreateService.java.tpl
DeleteService.java.tpl
DomainModel.java.tpl
DomainRepository.java.tpl
DomainService.java.tpl
DtoRequest.java.tpl
DtoResponse.java.tpl
EntityExceptionHandler.java.tpl
ErrorResponse.java.tpl
ExistException.java.tpl
GetService.java.tpl
GlobalExceptionHandler.java.tpl
JpaEntity.java.tpl
ListService.java.tpl
Mapper.java.tpl
NotFoundException.java.tpl
PageResponse.java.tpl
PersistenceAdapter.java.tpl
README.md
SpringDataRepository.java.tpl
UpdateService.java.tpl
scripts/examples/
product_with_rel.json
product.json

詳細

Spring Boot CRUD パターン

概要

機能に焦点を当てたアーキテクチャを使用して、Spring Boot 3.5 以降のサービス向けに完全な CRUD ワークフローを提供します。関心事を適切に分離しながら、ドメイン集約、JPA リポジトリ、アプリケーションサービス、REST コントローラーを作成して検証します。段階的に詳細を確認できるよう、詳細なコード一覧は参照ファイルに委ねます。

使用する場面

  • Spring Data JPA を使用した作成/読み取り/更新/削除ワークフロー向けの REST エンドポイントを作成します。
  • 集約、リポジトリ、アプリケーションサービスを備えた、DDD に着想を得たアーキテクチャに沿って機能パッケージを実装します。
  • 外部クライアント向けに DTO レコード、リクエストのバリデーション、コントローラーマッピングを定義します。
  • 既存の Spring Boot サービスにおける CRUD のデグレード、リポジトリの契約、トランザクション境界を診断します。
  • トリガーとなるフレーズ:「Spring の CRUD コントローラーを実装する」、「エンドポイントを作成する」、「データベースエンティティを追加する」、「機能ベースのリポジトリを改善する」、「JPA 集約の DTO をマッピングする」、「REST の一覧エンドポイントにページネーションを追加する」。

手順

機能に沿った CRUD サービスを、明示的な検証ゲートを設けながら提供するには、次の簡潔なワークフローに従います。

1. 機能の構成を確立する

feature/<name>/ ディレクトリを作成し、domain、application、presentation、infrastructure のサブパッケージを配置します。 検証:次に進む前に、ディレクトリ構成が機能の境界に合致していることを確認します。

2. ドメインモデルを定義する

ファクトリーメソッド(create、update)を通じて不変条件を適用するエンティティクラスを作成します。ドメインロジックにフレームワークを依存させないようにします。 検証:次に進む前に、すべての不変条件がユニットテストで網羅されていることを確認します。

3. ドメインポートを公開する

実装の詳細を含めずに永続化の契約を記述するリポジトリーインターフェイスを、domain/repository に宣言します。 検証:インターフェイスのシグネチャがドメイン操作と一致していることを確認します。

4. インフラストラクチャアダプターを提供する

ドメインモデルにマッピングする JPA エンティティを infrastructure/persistence に作成します。Spring Data リポジトリを実装します。 検証:@DataJpaTest を実行して、エンティティマッピングとリポジトリの統合を検証します。

5. アプリケーションサービスを実装する

ドメイン操作と DTO マッピングを調整する @Transactional サービスクラスを作成します。 検証:トランザクション境界が適切であり、必要な箇所に楽観的ロックが適用されていることを確認します。

6. DTO とコントローラーを定義する

API 契約には、jakarta.validation アノテーションを付けた Java レコードを使用します。適切なステータスコードを指定して REST エンドポイントをマッピングします。 検証:バリデーション制約をテストし、HTTP ステータスコード(POST は 201、GET は 200、DELETE は 204)を確認します。

7. 検証してデプロイする

Testcontainers を使用して統合テストを実行します。マイグレーション(Liquibase/Flyway)が集約スキーマと一致していることを確認します。 検証:デプロイ前にテストスイート全体を実行し、スキーママイグレーションスクリプトが適用されていることを確認します。

各手順に沿った完全なコードについては、references/examples-product-feature.md を参照してください。

例

Java コード例:Product 機能
// feature/product/domain/Product.java
package com.example.product.domain;

import java.math.BigDecimal;
import java.time.Instant;

public record Product(
    String id,
    String name,
    String description,
    BigDecimal price,
    int stock,
    Instant createdAt,
    Instant updatedAt
) {
    public static Product create(String name, String desc, BigDecimal price, int stock) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required");
        if (price == null || price.compareTo(BigDecimal.ZERO) < 0) throw new IllegalArgumentException("Invalid price");
        return new Product(null, name.trim(), desc, price, stock, Instant.now(), null);
    }

    public Product withPrice(BigDecimal newPrice) {
        return new Product(id, name, description, newPrice, stock, createdAt, Instant.now());
    }
}
// feature/product/domain/repository/ProductRepository.java
package com.example.product.domain.repository;

import com.example.product.domain.Product;
import java.util.Optional;

public interface ProductRepository {
    Product save(Product product);
    Optional<Product> findById(String id);
    void deleteById(String id);
}
// feature/product/infrastructure/persistence/ProductJpaEntity.java
package com.example.product.infrastructure.persistence;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;

@Entity @Table(name = "products")
public class ProductJpaEntity {
    @Id @GeneratedValue(strategy = GenerationType.UUID)
    private String id;
    private String name;
    private String description;
    private BigDecimal price;
    private int stock;
    private Instant createdAt;
    private Instant updatedAt;

    // getters, setters, constructor from domain (omitted for brevity)
}
// feature/product/infrastructure/persistence/JpaProductRepository.java
package com.example.product.infrastructure.persistence;

import com.example.product.domain.Product;
import com.example.product.domain.repository.ProductRepository;
import org.springframework.stereotype.Repository;

@Repository
public class JpaProductRepository implements ProductRepository {
    private final SpringDataProductRepository springData;

    public JpaProductRepository(SpringDataProductRepository springData) {
        this.springData = springData;
    }

    @Override
    public Product save(Product product) {
        ProductJpaEntity entity = toEntity(product);
        ProductJpaEntity saved = springData.save(entity);
        return toDomain(saved);
    }

    // findById, deleteById implementations...
}
// feature/product/presentation/rest/ProductController.java
package com.example.product.presentation.rest;

import com.example.product.domain.Product;
import com.example.product.domain.repository.ProductRepository;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController @RequestMapping("/api/products")
public class ProductController {
    private final ProductService service;

    public ProductController(ProductService service) { this.service = service; }

    @PostMapping
    public ResponseEntity<ProductResponse> create(@Valid @RequestBody CreateProductRequest req) {
        Product product = service.create(req.toDomain());
        return ResponseEntity.status(201).body(ProductResponse.from(product));
    }

    @GetMapping("/{id}")
    public ResponseEntity<ProductResponse> getById(@PathVariable String id) {
        return service.findById(id)
            .map(p -> ResponseEntity.ok(ProductResponse.from(p)))
            .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable String id) {
        service.deleteById(id);
        return ResponseEntity.noContent().build();
    }

    // record DTOs
    public record CreateProductRequest(
        @NotBlank String name,
        String description,
        @NotNull @DecimalMin("0.01") java.math.BigDecimal price,
        @Min(0) int stock
    ) {
        Product toDomain() { return Product.create(name, description, price, stock); }
    }

    public record ProductResponse(String id, String name, java.math.BigDecimal price) {
        static ProductResponse from(Product p) { return new ProductResponse(p.id(), p.name(), p.price()); }
    }
}
JSON 入出力の例

作成リクエスト:

{
  "name": "Wireless Keyboard",
  "description": "Ergonomic keyboard",
  "price": 79.99,
  "stock": 50
}

作成レスポンス(201):

{
  "id": "prod-123",
  "name": "Wireless Keyboard",
  "price": 79.99,
  "_links": { "self": "/api/products/prod-123" }
}

ページネーション付き一覧リクエスト:

curl "http://localhost:8080/api/products?page=0&size=10&sort=name,asc"

ベストプラクティス

  • 機能パッケージ内の各集約について、ドメイン、アプリケーション、プレゼンテーションのコードを同じ場所にまとめます。
  • イミュータブルな DTO には Java レコードを使用し、サービス境界でドメイン型に変換します。
  • 書き込み操作にはトランザクションと楽観的ロックを適用します。
  • ページネーションのデフォルト(page、size、sort)を標準化し、クエリパラメーターを文書化します。
  • CRUD のライフサイクルイベント(作成、更新、削除)を、構造化された監査証跡とともに info レベルでログに記録します。
  • Spring Boot Actuator を通じてヘルス情報とメトリクスを公開し、スループットとエラー率を監視します。

制約と警告

  • 遅延読み込みによる漏えいやシリアライズの問題を防ぐため、コントローラーでJPAエンティティを決して直接公開しないでください。
  • テスト容易性のための不変性を維持し、フィールドインジェクションとコンストラクターインジェクションを決して混在させないでください。
  • ビジネスロジックをコントローラーやリポジトリーアダプターに決して埋め込まず、ドメイン層またはアプリケーション層に配置してください。
  • 制約違反を防ぎ、一貫したエラーペイロードを生成するため、入力を必ず厳格に検証してください。
  • スキーマ変更をデプロイする前に、マイグレーション(Liquibase/Flyway)が集約の進化を反映していることを必ず確認してください。
  • 永続化のリグレッションを防ぐため、マージ前にTestcontainersを使って統合テストを必ず実行してください。

参考資料

コメント