0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Spring Data JPAでCRUDを作る ― DTO・Entity・Repositoryの境界

0
Last updated at Posted at 2026-08-25

Spring Data JPAでCRUDを作る ― DTO・Entity・Repositoryの境界

注文テーブルに列を一つ追加したら、APIのJSONまで変わってしまった。Entityをそのまま返す設計では、DBの変更が外部インターフェースへ漏れやすくなります。

この回では、DTO、Entity、Repository、DBを別の箱として扱います。

役割を先に分ける

要素 担当すること 担当しないこと
Request DTO HTTPで届いた入力を受ける DBの永続化状態を表す
Response DTO APIとして返す項目を決める Entityを自動的にすべて公開する
Entity JPAが永続化する状態を表す 画面・APIごとの表示都合を持つ
Repository Entityの保存・検索を依頼する 注文受付の業務手順を決める
DB データの保存、制約、検索、整合性 Serviceの代わりにユースケース全体を進行する

DTOは「箱同士で運ぶメッセージ」です。EntityはDBと対応する状態です。似た項目を持っていても、変更理由が違います。

DTOとEntityとDBの責務、およびN+1の比較

入力DTOでAPIの約束を表す

public record CreateOrderRequest(
        @NotBlank String productCode,
        @Min(1) int quantity) {
}

ControllerはHTTP入力を受け、Serviceへ渡すCommandへ変換します。

@RestController
@RequestMapping("/orders")
public class OrderController {
    private final OrderApplicationService service;

    public OrderController(OrderApplicationService service) {
        this.service = service;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> create(
            @Valid @RequestBody CreateOrderRequest request) {
        Order registered = service.register(
                new CreateOrderCommand(request.productCode(), request.quantity()));
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(OrderResponse.from(registered));
    }
}

@Validを付けると、Controllerメソッドへ入る前にBean Validationが入力を検証します。ただし「在庫があるか」「注文を受け付けてよいか」のような業務判断までDTOへ押し込みません。

Entityは永続化する状態を表す

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String productCode;

    @Column(nullable = false)
    private int quantity;

    protected Order() {}

    public Order(String productCode, int quantity) {
        this.productCode = productCode;
        this.quantity = quantity;
    }

    public Long getId() { return id; }
    public String getProductCode() { return productCode; }
    public int getQuantity() { return quantity; }
}

JPA用の引数なしコンストラクタはprotectedにし、業務コードから意味のない空Entityを作りにくくしています。

RepositoryはDBへの窓口

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByProductCode(String productCode);
}

Spring Data JPAのRepositoryインターフェースは、Springが実装を生成してBean登録します。そのため、このインターフェースへ@Repositoryを付けなくても動作します。

@Repositoryを省略できることと、Repositoryの役割が不要なことは別です

Spring Dataが実装とBean登録を支援しても、DBアクセスの窓口という設計上の役割は残ります。手書きの永続化クラスでは、@Repositoryを使って役割とBean登録を明示する場合があります。

EntityからResponse DTOへ詰め替える

public record OrderResponse(Long orderId, String productCode, int quantity) {
    public static OrderResponse from(Order order) {
        return new OrderResponse(
                order.getId(), order.getProductCode(), order.getQuantity());
    }
}

この変換が一段入ることで、DBに監査列や内部状態を追加してもAPIへ自動的に露出しません。逆にAPIの表示名を変えても、すぐにDB列名を変える必要はありません。

関連Entityは「いつ読むか」を決める

JPAで関連をたどれるからといって、必要なデータが効率よく読まれるとは限りません。注文一覧を1回で取り、その後に各注文の商品を遅延ロードすると、注文1回+商品N回のSQLになることがあります。これがN+1です。

一覧で商品情報も扱うモデルなら、Orderは次のような遅延関連を持ちます(前掲の最小Entityでは省略した部分です)。

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
private Product product;
@Query("""
       select o from Order o
       join fetch o.product
       """)
List<Order> findAllWithProduct();

join fetch@EntityGraphで必要な関連を明示できますが、常にJOINすればよいわけではありません。コレクションの重複、ページング、取得量が別の問題になります。APIごとに必要な項目と件数を決め、SQLログと実行計画を確認します。

本シリーズでは、spring.jpa.open-in-view=falseを基本にします。Controllerで遅延ロードへ偶然アクセスできる状態に頼らず、Serviceのトランザクション内で必要な関連を読み、Response DTOへ変換するためです。

DBとServiceの境界

DBには、NOT NULL、UNIQUE、外部キーなど、データそのものの整合性を守る制約を置きます。Serviceは「入力を確認し、注文を作り、保存し、結果を返す」というユースケースを進めます。

DB制約は最後の砦であり、Serviceは利用者に意味のある判断とエラーを返す場所です。どちらか一方へすべて寄せる話ではありません。

Java 17、Spring Boot 3.5.16、H2の検証プロジェクトで、POSTの201応答、数量0のValidationエラー、Entity保存、Response DTOへの変換を確認しました。ここでH2を使ったのはサンプルの配線確認です。DB製品固有の型、ロック、SQLは本番と同じDBでも検証します。

実務ではこうなる

EntityをJSONへ直接変換すると、双方向関連による循環、意図しない遅延ロード、内部列の露出が同時に起きやすくなります。@JsonIgnoreを増やして抑えるより、APIの契約をResponse DTOとして明示するほうが変更箇所を追いやすくなります。

DB制約違反は、事前確認だけでは防ぎ切れません。同時更新があるからです。Serviceで利用者向けの判断をしつつ、DBにもNOT NULL、UNIQUE、外部キーを置き、最後はDB例外をAPIのエラーへ翻訳します。

また、save()の呼び過ぎで安全になるわけではありません。トランザクション内で取得したEntityは変更検出の対象です。いつINSERT・UPDATEが発行されるかは採番方式やflushに左右されるため、第6回と第8回で境界を確認します。

公式リファレンス

参考文献・参照資料

  • Somnath Musib, Spring Boot in Practice, Manning, ISBN 9781617298813:第3章のSpring Dataを参照。
  • Mark Heckler, Spring Boot:立ち上げて実行する、ISBN 9798341626911(AI翻訳版):第4章・第6章のデータベースとデータ処理を補助参照。
  • 田村達也『後悔しないためのSpring Boot入門書:Spring解体新書(第2版)』:JPA・入力チェックの説明を参照。

書籍のコード・図・訳文は転載せず、注文受付のDTO・Entityへ置き換えました。SQLとJPAの現在の仕様は公式資料と実行結果を優先しています。

次回は、在庫更新、注文保存、通知予約を一つの仕事として守るトランザクション境界を扱います。

シリーズ内リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?