Spring Data JPAデータ永続化
JPAにより, オブジェクトとリレーショナルデータベースのマッピングが宣言的になります。Entityを書き, Repositoryを定義すれば, SQLが自動生成されます。
1. 学ぶ内容
- JPA Entity定義:
@Entity/@Id/@GeneratedValue/@Columnアノテーション - Repositoryインターフェース:
CrudRepository/JpaRepository/ カスタムクエリメソッド @OneToMany/@ManyToOneEntity関係マッピング (Order ↔ OrderItem)@QueryJPQLとネイティブSQLクエリ- データベース初期化:
schema.sql/data.sqlとHibernateddl-auto戦略
2. データ永続化に関する実話
(1) ペインポイント:インメモリデータは再起動で消える
Aliceはこれまでインメモリマップを使ってOrderFlowの商品と注文のデータを格納していました。アプリケーションを再起動するたびにすべてのデータが消え, テストのたびに手動で商品を再作成しなければなりませんでした。さらに困るのは, マップでは複雑なクエリ (日付範囲での注文検索など)がサポートされず, 本番環境では実際のデータベースを使用する必要がありました。
(2) Spring Data JPAによる解決策
Spring Data JPAにより, 永続化は「インターフェースを定義するだけ」になります。
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
@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は省略
}
出力:
// 実行成功
ddl-autoの値 |
動作 | 適用環境 |
|---|---|---|
create-drop |
起動時にテーブル作成, 終了時にテーブル削除 | テスト |
create |
毎回起動時にテーブル再構築 | 開発 |
update |
追加のみ, 削除なし。列の変更 | 開発 |
validate |
検証のみ, 変更しない | 本番 |
none |
何もしない | 本番 (手動管理) |
validateまたはnoneを使用してください。updateはデータ損失の可能性があります。
4. Repositoryインターフェース
(1) Repository継承階層
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
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);
}
出力:
// 実行成功
(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:一対多関係
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
@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() {}
}
出力:
// 実行成功
| カスケードタイプ | 意味 | 推奨される使用 |
|---|---|---|
ALL |
すべての操作をカスケード | 強い依存関係のみで使用 |
PERSIST |
永続化のカスケード | 一般的 |
MERGE |
マージのカスケード | 一般的 |
REMOVE |
削除のカスケード | 注意して使用 |
| フェッチタイプ | 動作 | 使用場面 |
|---|---|---|
LAZY |
アクセス時のみクエリ | N+1回避のため推奨 |
EAGER |
即時読み込み | @ManyToOneのデフォルト値 |
6. @Query JPQL vs. ネイティブSQL
(1) ▶ サンプル:JPQLとネイティブSQLクエリ
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();
}
出力:
// 実行成功
| 項目 | JPQL | ネイティブSQL |
|---|---|---|
| 構文 | Entityとフィールドベース | テーブルとカラムベース |
| データベース移行 | 移植可能 | 移植不可 |
| 機能 | 制限あり (UNIONなどをサポートしない) | フル機能 |
| 戻り値の型 | Entity / DTO | Object[] / DTO |
7. データベース初期化
(1) ▶ サンプル:schema.sqlとdata.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
);
出力:
CREATE TABLE
-- 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永続化の完全な実装
// 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);
}
❓ よくある質問
protectedでアノテートすることで外部からの誤用を防げます。recordには引数なしコンストラクタがないため, JPA Entityとして直接使用できません。@Transactionalでセッションを開いたままにする;2)JOIN FETCHで一括読み込み;3)@EntityGraphで読み込み計画を定義する。ddl-auto=updateは安全ですか?updateオプションは挿入のみで削除しないため, 古い列が残る可能性があります。また, 列の型変更はデータ損失の原因になります。本番環境ではvalidateとFlywayまたはLiquibaseでデータベース移行を管理してください。@Query("JOIN FETCH")で関連データを一括読み込み;2)@EntityGraphで宣言的読み込み計画;3)@BatchSizeでバッチ読み込み。これがJPAパフォーマンス最適化の最も重要なポイントです。ddl-auto=noneに設定してください。📖 まとめ
- JPA Entityはアノテーションでオブジェクトをリレーショナルテーブルにマッピング。
@Id+@GeneratedValueで主キーを管理 JpaRepositoryは完全なCRUD機能 + ページネーション + ソートを提供。メソッド命名規則に基づいてクエリを自動生成@OneToMany/@ManyToOneでEntity関係をマッピング。カスケードとフェッチ戦略に注意@QueryJPQLはEntityクエリに適し, ネイティブSQLは複雑な統計に適している- 本番環境では
validate+ 移行ツール (Flyway/Liquibase)を使用。ddl-auto=updateは使用しない
📝 練習問題
-
基本問題 (難易度 ⭐):OrderFlowにH2データソースを設定し, Product EntityとProductRepositoryを作成し, 商品のCRUD REST APIを実装してH2にデータを永続化してください。
-
応用問題 (難易度 ⭐⭐):
OrderとOrderItemの一対多関係を実装し,JOIN FETCHでN+1問題を解決し, 日付範囲で注文を照会するRepositoryメソッドを書いてください。 -
チャレンジ問題 (難易度 ⭐⭐⭐):MySQLデータソースに切り替え, Flywayでデータベース移行スクリプトを管理し, V1__init_schema.sqlとV2__add_order_status_index.sqlの移行ファイルを作成してください。



