404 Not Found

404 Not Found


nginx

Spring Data JPAデータ永続化

JPAにより, オブジェクトとリレーショナルデータベースのマッピングが宣言的になります。Entityを書き, Repositoryを定義すれば, SQLが自動生成されます。

1. 学ぶ内容


2. データ永続化に関する実話

(1) ペインポイント:インメモリデータは再起動で消える

Aliceはこれまでインメモリマップを使ってOrderFlowの商品と注文のデータを格納していました。アプリケーションを再起動するたびにすべてのデータが消え, テストのたびに手動で商品を再作成しなければなりませんでした。さらに困るのは, マップでは複雑なクエリ (日付範囲での注文検索など)がサポートされず, 本番環境では実際のデータベースを使用する必要がありました。

(2) Spring Data JPAによる解決策

Spring Data JPAにより, 永続化は「インターフェースを定義するだけ」になります。

JAVA
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByNameContaining(String keyword);
}

実装クラスなしで, Spring Dataが自動的にSQLを生成します。

(3) 成果

AliceがJPAを導入した後, OrderFlowのデータはMySQLに永続化され, 再起動後も保持されます。複雑なクエリはメソッド命名規則や@Queryで実装され, Repositoryインターフェースには実装コードが含まれません。


3. JPA Entityの定義

(1) コアEntityアノテーション

アノテーション 用途
@Entity JPA Entityとしてマーク @Entity public class Product
@Table テーブル名の指定 @Table(name = "products")
@Id 主キーのマーク @Id private Long id;
@GeneratedValue 主キー生成戦略 @GeneratedValue(strategy = IDENTITY)
@Column 列マッピング @Column(nullable = false, length = 200)
@CreationTimestamp 作成日時の自動設定 @CreationTimestamp private Instant createdAt;

(1) ▶ サンプル:Product Entity

JAVA
@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Column(nullable = false)
    private Integer stock;

    @CreationTimestamp
    private Instant createdAt;

    // JPAで必要なデフォルトコンストラクタ
    protected Product() {}

    public Product(String name, BigDecimal price, Integer stock) {
        this.name = name;
        this.price = price;
        this.stock = stock;
    }

    // getter/setterは省略
}

出力:

TEXT
// 実行成功
ddl-autoの値 動作 適用環境
create-drop 起動時にテーブル作成, 終了時にテーブル削除 テスト
create 毎回起動時にテーブル再構築 開発
update 追加のみ, 削除なし。列の変更 開発
validate 検証のみ, 変更しない 本番
none 何もしない 本番 (手動管理)
⚠️ 注意: 本番環境ではvalidateまたはnoneを使用してください。updateはデータ損失の可能性があります。


4. Repositoryインターフェース

(1) Repository継承階層

100%
graph LR
    A[Repository] --> B[CrudRepository]
    B --> C[ListCrudRepository]
    B --> D[PagingAndSortingRepository]
    D --> E[JpaRepository]
    C --> E
インターフェース 主要メソッド 使用場面
CrudRepository save / findById / findAll / delete 基本的なCRUD
ListCrudRepository findAllがListを返す Iterable変換の回避
PagingAndSortingRepository findAll(Pageable) ページネーションとソート
JpaRepository 上記すべて + flush / saveAndFlush 最も一般的に使用, 推奨

(1) ▶ サンプル:ProductRepository

JAVA
public interface ProductRepository extends JpaRepository<Product, Long> {

    // メソッド名による派生クエリ
    List<Product> findByNameContaining(String keyword);

    List<Product> findByStockLessThan(Integer threshold);

    List<Product> findByPriceBetween(BigDecimal min, BigDecimal max);

    // カスタムJPQL
    @Query("SELECT p FROM Product p WHERE p.stock = 0")
    List<Product> findOutOfStockProducts();

    // ネイティブSQL
    @Query(value = "SELECT * FROM products WHERE price > :minPrice ORDER BY price DESC",
           nativeQuery = true)
    List<Product> findExpensiveProducts(@Param("minPrice") BigDecimal minPrice);
}

出力:

TEXT
// 実行成功

(2) メソッド名の規則

キーワード 生成されるSQL
findBy findByName WHERE name = ?
Containing findByNameContaining WHERE name LIKE %?%
Between findByPriceBetween WHERE price BETWEEN ? AND ?
LessThan findByStockLessThan WHERE stock < ?
OrderBy findByPriceOrderByStockDesc ORDER BY stock DESC
And / Or findByNameAndStock WHERE name = ? AND stock = ?

5. Entity関係マッピング

(1) Order ↔ OrderItem:一対多関係

100%
erDiagram
    ORDER ||--o{ ORDER_ITEM : 含む
    PRODUCT ||--o{ ORDER_ITEM : 含まれる
    ORDER {
        bigint id PK
        varchar status
        decimal total_amount
        timestamp created_at
    }
    ORDER_ITEM {
        bigint id PK
        bigint order_id FK
        bigint product_id FK
        int quantity
        decimal unit_price
    }
    PRODUCT {
        bigint id PK
        varchar name
        decimal price
        int stock
    }

(1) ▶ サンプル:OrderとOrderItem Entity

JAVA
@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 20)
    private String status;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal totalAmount;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @CreationTimestamp
    private Instant createdAt;

    protected Order() {}

    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
    }
}

@Entity
@Table(name = "order_items")
public class OrderItem {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id", nullable = false)
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id", nullable = false)
    private Product product;

    @Column(nullable = false)
    private Integer quantity;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal unitPrice;

    protected OrderItem() {}
}

出力:

TEXT
// 実行成功
カスケードタイプ 意味 推奨される使用
ALL すべての操作をカスケード 強い依存関係のみで使用
PERSIST 永続化のカスケード 一般的
MERGE マージのカスケード 一般的
REMOVE 削除のカスケード 注意して使用
フェッチタイプ 動作 使用場面
LAZY アクセス時のみクエリ N+1回避のため推奨
EAGER 即時読み込み @ManyToOneのデフォルト値

6. @Query JPQL vs. ネイティブSQL

(1) ▶ サンプル:JPQLとネイティブSQLクエリ

JAVA
public interface OrderRepository extends JpaRepository<Order, Long> {

    // JPQL:ステータスで注文を検索し項目数を取得
    @Query("SELECT o FROM Order o WHERE o.status = :status")
    List<Order> findByStatus(@Param("status") String status);

    // JPQL:JOIN FETCHでN+1問題を回避
    @Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
    Optional<Order> findByIdWithItems(@Param("id") Long id);

    // ネイティブSQL:注文統計
    @Query(value = """
        SELECT o.status, COUNT(*) as cnt, SUM(o.total_amount) as total
        FROM orders o
        GROUP BY o.status
        """, nativeQuery = true)
    List<Object[]> getOrderStatistics();
}

出力:

TEXT
// 実行成功
項目 JPQL ネイティブSQL
構文 Entityとフィールドベース テーブルとカラムベース
データベース移行 移植可能 移植不可
機能 制限あり (UNIONなどをサポートしない) フル機能
戻り値の型 Entity / DTO Object[] / DTO

7. データベース初期化

(1) ▶ サンプル:schema.sqlとdata.sql

SQL
-- src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS products (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    total_amount DECIMAL(10,2) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

出力:

TEXT
CREATE TABLE
SQL
-- src/main/resources/data.sql
INSERT INTO products (name, price, stock) VALUES
    ('Laptop Pro', 1299.99, 50),
    ('Wireless Mouse', 29.99, 200),
    ('USB-C Hub', 49.99, 100);
初期化方法 設定 使用場面
Hibernate ddl-auto spring.jpa.hibernate.ddl-auto=update 開発
schema.sql + data.sql spring.sql.init.mode=always DDLの正確な制御が必要な場合
Flyway spring.flyway.enabled=true 本番移行
Liquibase spring.liquibase.enabled=true 本番移行

8. 総合サンプル:OrderFlow JPA永続化の完全な実装

JAVA
// Product.java
@Entity
@Table(name = "products")
public class Product {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Column(nullable = false)
    private Integer stock;

    @CreationTimestamp
    private Instant createdAt;

    protected Product() {}
    public Product(String name, BigDecimal price, Integer stock) {
        this.name = name; this.price = price; this.stock = stock;
    }
    // getters/setters
}

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

    @Column(nullable = false, length = 20)
    private String status = "PENDING";

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal totalAmount = BigDecimal.ZERO;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @CreationTimestamp
    private Instant createdAt;

    protected Order() {}
    public void addItem(Product product, int quantity) {
        OrderItem item = new OrderItem(this, product, quantity, product.getPrice());
        items.add(item);
        totalAmount = totalAmount.add(product.getPrice().multiply(BigDecimal.valueOf(quantity)));
    }
    // getters/setters
}

// OrderItem.java
@Entity
@Table(name = "order_items")
public class OrderItem {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;

    @Column(nullable = false)
    private Integer quantity;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal unitPrice;

    protected OrderItem() {}
    public OrderItem(Order order, Product product, Integer quantity, BigDecimal unitPrice) {
        this.order = order; this.product = product;
        this.quantity = quantity; this.unitPrice = unitPrice;
    }
    // getters/setters
}

// ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> {
    List<Product> findByNameContaining(String keyword);
    List<Product> findByStockLessThan(Integer threshold);
}

❓ よくある質問

Q なぜJPA Entityには引数なしコンストラクタが必要ですか?
A JPAはリフレクションを使ってEntityインスタンスを作成するため, 引数なしコンストラクタが必要です。protectedでアノテートすることで外部からの誤用を防げます。recordには引数なしコンストラクタがないため, JPA Entityとして直接使用できません。
Q LazyInitializationExceptionの解決方法は?
A 原因はセッションが閉じた後に遅延読み込みプロパティにアクセスしたことです。解決策:1)@Transactionalでセッションを開いたままにする;2)JOIN FETCHで一括読み込み;3)@EntityGraphで読み込み計画を定義する。
Q CrudRepositoryとJpaRepositoryはどちらを選ぶべきですか?
A JpaRepositoryを推奨します。すべてのサブインターフェースを継承し, 最も包括的なメソッドセット (ページネーション, ソート, 一括操作を含む)を提供するため, 実際の開発での標準的な選択です。
Q ddl-auto=updateは安全ですか?
A 安全ではありません。updateオプションは挿入のみで削除しないため, 古い列が残る可能性があります。また, 列の型変更はデータ損失の原因になります。本番環境ではvalidateとFlywayまたはLiquibaseでデータベース移行を管理してください。
Q N+1クエリ問題の対処方法は?
A 1)@Query("JOIN FETCH")で関連データを一括読み込み;2)@EntityGraphで宣言的読み込み計画;3)@BatchSizeでバッチ読み込み。これがJPAパフォーマンス最適化の最も重要なポイントです。
Q schema.sqlとHibernateのddl-autoは併用できますか?
A 併用は推奨されません。競合する可能性があります。HibernateでDDLを管理する場合はschema.sqlは不要です。schema.sqlを使う場合はddl-auto=noneに設定してください。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度 ⭐):OrderFlowにH2データソースを設定し, Product EntityとProductRepositoryを作成し, 商品のCRUD REST APIを実装してH2にデータを永続化してください。

  2. 応用問題 (難易度 ⭐⭐):OrderOrderItemの一対多関係を実装し, JOIN FETCHでN+1問題を解決し, 日付範囲で注文を照会するRepositoryメソッドを書いてください。

  3. チャレンジ問題 (難易度 ⭐⭐⭐):MySQLデータソースに切り替え, Flywayでデータベース移行スクリプトを管理し, V1__init_schema.sqlとV2__add_order_status_index.sqlの移行ファイルを作成してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%