前回はSpring MVCを使った実践的なWebアプリケーション構築について解説しました。今回は、エンタープライズアプリケーション1に不可欠なデータアクセス層の実装について、Spring Data JPAの基礎を中心に解説します。
Spring Data JPAとは
Spring Data JPAは、JPA2ベースのリポジトリ層3を簡単に実装するためのフレームワークです。ボイラープレートコード4を大幅に削減し、データアクセス層の実装を効率化します。
データベースを使うアプリケーションでは、データの保存や取得のために多くのコードを書く必要があります。Spring Data JPAは、これらの定型的なコードを自動的に生成してくれるため、開発者は「商品を保存する」「IDで商品を探す」といった基本的な操作のコードを自分で書く必要がなくなります。
リクエスト処理
━━━━━━━━━━
Webブラウザからの
リクエストを受け取る窓口\”]\n Service[\”@Service
ビジネスロジック
トランザクション境界
━━━━━━━━━━
実際の処理を行う場所
例:在庫チェック、注文処理\”]\n Repository[\”Repository Interface
━━━━━━━━━━
インターフェース定義のみ
findByName等のメソッド定義
━━━━━━━━━━
データベース操作の
指示書を書く場所\”]\n SpringDataJPA[\”Spring Data JPA
━━━━━━━━━━
実装の自動生成
メソッド名からクエリ生成
ページネーション処理
━━━━━━━━━━
指示書を実際の
SQL文に変換する翻訳機\”]\n JPA[\”JPA Provider (Hibernate)
━━━━━━━━━━
O/Rマッピング
@Queryの実行
━━━━━━━━━━
JavaオブジェクトとDBの
データを相互変換\”]\n DB[(\”Relational Database
MySQL/PostgreSQL等
━━━━━━━━━━
データを実際に
保管する倉庫\”)]\n \n Controller –> Service\n Service –> Repository\n Repository –> SpringDataJPA\n SpringDataJPA –> JPA\n JPA –> DB\n DB –> JPA\n JPA –> SpringDataJPA\n SpringDataJPA –> Repository\n Repository –> Service\n Service –> Controller\n \n style Controller fill:#ff9999,stroke:#333,stroke-width:2px\n style Service fill:#99ccff,stroke:#333,stroke-width:2px\n style Repository fill:#99ff99,stroke:#333,stroke-width:2px\n style SpringDataJPA fill:#ffffcc,stroke:#333,stroke-width:3px\n style JPA fill:#99ff99,stroke:#333,stroke-width:2px\n style DB fill:#ffcc99,stroke:#333,stroke-width:2px”,”key”:”1f6c9f103480d31552d4ca5c425845da”}”>
主な特徴
- リポジトリの自動実装: インターフェースを定義するだけで基本的なCRUD5操作が可能
-
メソッド名からクエリを自動生成:
findByName
のようなメソッド名からSQL6を生成 - ページネーション7・ソート機能: 大量データの効率的な処理
-
カスタムクエリのサポート: 複雑なクエリも
@Query
アノテーションで定義可能
プロジェクトのセットアップ
まず、Spring Data JPAを使うために必要な依存関係を追加します。
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
com.mysql
mysql-connector-j
runtime
com.h2database
h2
runtime
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
アプリケーション設定
# application.yml
spring:
datasource:
# 開発環境ではH2インメモリデータベースを使用
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
# Hibernateの設定
hibernate:
ddl-auto: create-drop # 起動時にテーブルを作成、終了時に削除
show-sql: true # 実行されるSQLをコンソールに表示
properties:
hibernate:
format_sql: true # SQLを見やすく整形
h2:
console:
enabled: true # H2コンソールを有効化(http://localhost:8080/h2-console)
エンティティクラスの設計
JPAエンティティ8は、データベースのテーブルとマッピングされるJavaクラスです。実際の商品管理システムを例に、詳しく見ていきましょう。
エンティティクラスは、データベースのテーブルに保存するデータの設計図のようなものです。例えば「商品」テーブルには商品名、価格、在庫数などの情報が必要ですが、これらをJavaのクラスとして定義します。
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity // このクラスがJPAエンティティであることを示す
@Table(name = "products") // 対応するテーブル名を指定
@EntityListeners(AuditingEntityListener.class) // 監査機能を有効化
@Getter // Lombokでgetterメソッドを自動生成
@Setter // Lombokでsetterメソッドを自動生成
public class Product {
@Id // 主キー(各商品を一意に識別するID)であることを示す
@GeneratedValue(strategy = GenerationType.IDENTITY) // DBの自動採番を使用
private Long id;
@Column(nullable = false, length = 100) // NOT NULL制約、最大100文字
private String name; // 商品名
@Column(nullable = false) // NOT NULL制約(必須項目)
private Integer price; // 価格
@Column(length = 500) // 最大500文字(NULLは許可)
private String description; // 商品説明(任意項目)
@Column(nullable = false)
private Integer stockQuantity = 0; // 在庫数(デフォルト値を0に設定)
// 楽観的ロック用のバージョン番号
// 更新時に自動的にインクリメントされ、同時更新を検出
// 例:AさんとBさんが同時に在庫を更新しようとした時の衝突を防ぐ
@Version
private Long version;
// 作成日時を自動記録(監査機能)
// いつこの商品が登録されたかを自動的に記録
@CreatedDate
@Column(nullable = false, updatable = false) // 更新不可
private LocalDateTime createdAt;
// 更新日時を自動記録(監査機能)
// いつこの商品情報が最後に更新されたかを自動的に記録
@LastModifiedDate
private LocalDateTime updatedAt;
// カテゴリとの多対1の関係
// 複数の商品が1つのカテゴリに属する(例:「書籍」カテゴリに複数の本)
// FetchType.LAZY: 必要になるまでカテゴリをロードしない(パフォーマンス対策)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id") // 外部キーカラム名
private Category category;
// ビジネスメソッドの例:在庫を減らす
// エンティティに業務ロジックを含めることで、データの整合性を保つ
public void decrementStock(int quantity) {
if (this.stockQuantity quantity) {
throw new IllegalArgumentException("在庫が不足しています");
}
this.stockQuantity -= quantity;
}
// 在庫を増やす
public void incrementStock(int quantity) {
this.stockQuantity += quantity;
}
}
カテゴリエンティティ
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "categories")
@Getter
@Setter
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String name; // カテゴリ名(重複不可)
@Column(length = 200)
private String description; // カテゴリの説明
// 商品との1対多の関係
// 1つのカテゴリに複数の商品が属する
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL)
private ListProduct> products = new ArrayList();
}
エンティティの関連マッピング
データベースでは、テーブル同士が関連を持つことがよくあります。例えば、商品はカテゴリに属し、注文には複数の商品が含まれます。このような関係をER図で表すと以下のようになります。
エンティティ設計のポイント
-
@Version
による楽観的ロック9: 同時更新による不整合を防ぐ仕組み。例えば、2人が同時に在庫を更新しようとした時、後から更新した人にエラーを返して、データの不整合を防ぎます。 -
監査機能10の活用:
@CreatedDate
、@LastModifiedDate
で自動的に日時を記録。「いつ誰が作成・更新したか」という履歴を自動で残せます。 -
適切なフェッチ戦略11:
FetchType.LAZY
で遅延ロード。必要な時だけ関連データを取得することで、パフォーマンスを向上させます。 -
Lombok12の活用:
@Getter
や@Setter
で、getterやsetterメソッドを自動生成。手作業でこれらのメソッドを書く必要がなくなります。
監査機能を有効にする設定
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing // JPA監査機能を有効化
public class JpaConfig {
// Spring Bootが自動的に設定を行うため、通常は追加の設定は不要
// この設定により、@CreatedDateや@LastModifiedDateが自動的に動作する
}
リポジトリインターフェースの実装
Spring Data JPAでは、JpaRepository
を継承するだけで基本的なCRUD操作が可能になります。実装クラスは実行時に自動生成されます。
リポジトリは、データベースとのやり取りを担当する部分です。商品を倉庫(データベース)から取り出したり、新しい商品を倉庫に保管したりする「倉庫係」のような役割を持ちます。
基本的なリポジトリインターフェース
package com.example.demo.repository;
import com.example.demo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ProductRepository extends JpaRepositoryProduct, Long> {
// JpaRepositoryを継承することで、以下のメソッドが自動的に利用可能:
// ===== 基本的なCRUD操作(自動で提供される) =====
// save(Product entity) - データの保存・更新
// findById(Long id) - IDでデータを検索
// findAll() - 全データを取得
// deleteById(Long id) - IDでデータを削除
// count() - データ件数を取得
// existsById(Long id) - IDのデータが存在するか確認
// ===== カスタムクエリメソッド(メソッド名から自動生成) =====
// 商品名で検索
// SQLが自動生成される: SELECT * FROM products WHERE name = ?
OptionalProduct> findByName(String name);
// 商品名に特定の文字列を含む商品を検索
// 例:findByNameContaining("パン") → "パン"を含む商品を検索
// SQL: SELECT * FROM products WHERE name LIKE '%パン%'
ListProduct> findByNameContaining(String keyword);
// 価格範囲で検索
// SQL: SELECT * FROM products WHERE price >= ? AND price
ListProduct> findByPriceBetween(Integer minPrice, Integer maxPrice);
// 在庫がある商品を価格の安い順で取得
// SQL: SELECT * FROM products WHERE stock_quantity > 0 ORDER BY price ASC
ListProduct> findByStockQuantityGreaterThanOrderByPriceAsc(Integer stockQuantity);
// カテゴリIDで商品を検索
// SQL: SELECT * FROM products WHERE category_id = ?
ListProduct> findByCategoryId(Long categoryId);
}
カテゴリリポジトリ
package com.example.demo.repository;
import com.example.demo.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface CategoryRepository extends JpaRepositoryCategory, Long> {
// カテゴリ名で検索(一意制約があるため、Optionalで返す)
OptionalCategory> findByName(String name);
}
クエリメソッドの命名規則
Spring Data JPAでは、メソッド名から自動的にSQLを生成します。この命名規則を理解すると、様々な検索条件を簡単に実装できます。
よく使うクエリメソッドのパターン
@Repository
public interface ProductRepository extends JpaRepositoryProduct, Long> {
// ===== 基本的な検索 =====
ListProduct> findByName(String name); // 完全一致
ListProduct> findByNameIgnoreCase(String name); // 大文字小文字を無視
// ===== 部分一致検索 =====
ListProduct> findByNameContaining(String keyword); // 部分一致
ListProduct> findByNameStartingWith(String prefix); // 前方一致
ListProduct> findByNameEndingWith(String suffix); // 後方一致
// ===== 比較演算子 =====
ListProduct> findByPriceLessThan(Integer price); // より小さい
ListProduct> findByPriceLessThanEqual(Integer price); // 以下
ListProduct> findByPriceGreaterThan(Integer price); // より大きい
ListProduct> findByPriceGreaterThanEqual(Integer price); // 以上
// ===== 複数条件 =====
ListProduct> findByNameAndPrice(String name, Integer price); // AND条件
ListProduct> findByNameOrPrice(String name, Integer price); // OR条件
// ===== NULL チェック =====
ListProduct> findByDescriptionIsNull(); // NULLのもの
ListProduct> findByDescriptionIsNotNull(); // NULLでないもの
// ===== IN句 =====
ListProduct> findByNameIn(ListString> names); // 複数の値のいずれか
// ===== ソート =====
ListProduct> findByOrderByPriceAsc(); // 価格の昇順
ListProduct> findByOrderByPriceDesc(); // 価格の降順
// ===== 複雑な条件 =====
ListProduct> findByNameContainingAndPriceLessThanOrderByPriceDesc(
String keyword, Integer maxPrice
); // 名前に特定文字を含み、価格が指定値未満の商品を価格の高い順で取得
}
サービス層の実装
リポジトリを使用して、ビジネスロジックを実装するサービス層を作成します。
package com.example.demo.service;
import com.example.demo.entity.Product;
import com.example.demo.entity.Category;
import com.example.demo.repository.ProductRepository;
import com.example.demo.repository.CategoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor // finalフィールドのコンストラクタを自動生成
public class ProductService {
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
// 商品を登録する
@Transactional
public Product createProduct(String name, Integer price,
String description, Long categoryId) {
// カテゴリの存在確認
Category category = categoryRepository.findById(categoryId)
.orElseThrow(() -> new IllegalArgumentException(
"カテゴリが見つかりません: " + categoryId));
// 商品名の重複チェック
productRepository.findByName(name).ifPresent(existing -> {
throw new IllegalArgumentException(
"同名の商品が既に存在します: " + name);
});
// 新しい商品を作成
Product product = new Product();
product.setName(name);
product.setPrice(price);
product.setDescription(description);
product.setCategory(category);
product.setStockQuantity(0); // 初期在庫は0
// データベースに保存
return productRepository.save(product);
}
// 全商品を取得
@Transactional(readOnly = true) // 読み取り専用トランザクション
public ListProduct> getAllProducts() {
return productRepository.findAll();
}
// 商品をID検索
@Transactional(readOnly = true)
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException(
"商品が見つかりません: " + id));
}
// 商品名で検索
@Transactional(readOnly = true)
public ListProduct> searchProductsByName(String keyword) {
return productRepository.findByNameContaining(keyword);
}
// 価格範囲で検索
@Transactional(readOnly = true)
public ListProduct> searchProductsByPriceRange(Integer minPrice,
Integer maxPrice) {
return productRepository.findByPriceBetween(minPrice, maxPrice);
}
// 在庫を更新
@Transactional
public Product updateStock(Long productId, Integer quantity) {
Product product = getProductById(productId);
if (quantity > 0) {
product.incrementStock(quantity);
} else {
product.decrementStock(-quantity);
}
return productRepository.save(product);
}
// 商品を削除
@Transactional
public void deleteProduct(Long productId) {
// 存在確認
if (!productRepository.existsById(productId)) {
throw new IllegalArgumentException(
"商品が見つかりません: " + productId);
}
productRepository.deleteById(productId);
}
}
コントローラの実装
RESTful APIとして商品管理機能を公開します。
package com.example.demo.controller;
import com.example.demo.entity.Product;
import com.example.demo.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
// 商品作成のリクエストDTO
@lombok.Data
public static class CreateProductRequest {
private String name;
private Integer price;
private String description;
private Long categoryId;
}
// 在庫更新のリクエストDTO
@lombok.Data
public static class UpdateStockRequest {
private Integer quantity;
}
// 商品を作成
@PostMapping
public ResponseEntityProduct> createProduct(
@RequestBody CreateProductRequest request) {
Product product = productService.createProduct(
request.getName(),
request.getPrice(),
request.getDescription(),
request.getCategoryId()
);
return ResponseEntity.status(HttpStatus.CREATED).body(product);
}
// 全商品を取得
@GetMapping
public ListProduct> getAllProducts() {
return productService.getAllProducts();
}
// 商品をID検索
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.getProductById(id);
}
// 商品名で検索
@GetMapping("/search")
public ListProduct> searchProducts(@RequestParam String keyword) {
return productService.searchProductsByName(keyword);
}
// 価格範囲で検索
@GetMapping("/search/price")
public ListProduct> searchProductsByPrice(
@RequestParam Integer minPrice,
@RequestParam Integer maxPrice) {
return productService.searchProductsByPriceRange(minPrice, maxPrice);
}
// 在庫を更新
@PatchMapping("/{id}/stock")
public Product updateStock(
@PathVariable Long id,
@RequestBody UpdateStockRequest request) {
return productService.updateStock(id, request.getQuantity());
}
// 商品を削除
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
}
}
動作確認
アプリケーションを起動して、実際に動作を確認してみましょう。
初期データの投入
package com.example.demo;
import com.example.demo.entity.Category;
import com.example.demo.repository.CategoryRepository;
import com.example.demo.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {
private final CategoryRepository categoryRepository;
private final ProductService productService;
@Override
public void run(String... args) throws Exception {
// カテゴリを作成
Category books = new Category();
books.setName("書籍");
books.setDescription("技術書、ビジネス書など");
books = categoryRepository.save(books);
Category electronics = new Category();
electronics.setName("家電");
electronics.setDescription("生活家電、PC周辺機器など");
electronics = categoryRepository.save(electronics);
// 商品を作成
productService.createProduct(
"Spring Boot入門", 3000,
"Spring Bootの基礎を学ぶ", books.getId()
);
productService.createProduct(
"Java実践ガイド", 3500,
"Javaの実践的な使い方を解説", books.getId()
);
productService.createProduct(
"ワイヤレスマウス", 2500,
"Bluetooth対応のマウス", electronics.getId()
);
}
}
APIの使用例
# 全商品を取得
curl http://localhost:8080/api/products
# 商品を作成
curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{
"name": "キーボード",
"price": 5000,
"description": "メカニカルキーボード",
"categoryId": 2
}'
# 商品名で検索
curl http://localhost:8080/api/products/search?keyword=Spring
# 在庫を更新(10個追加)
curl -X PATCH http://localhost:8080/api/products/1/stock \
-H "Content-Type: application/json" \
-d '{"quantity": 10}'
H2コンソールでデータ確認
ブラウザで http://localhost:8080/h2-console
にアクセスし、以下の情報でログイン:
- JDBC URL:
jdbc:h2:mem:testdb
- User Name:
sa
- Password: (空欄)
SQLを実行してデータを確認できます:
-- 全商品を表示
SELECT * FROM PRODUCTS;
-- カテゴリ別の商品数
SELECT c.NAME, COUNT(p.ID)
FROM CATEGORIES c
LEFT JOIN PRODUCTS p ON c.ID = p.CATEGORY_ID
GROUP BY c.NAME;
まとめ
今回は、Spring Data JPAの基礎について解説しました。
本記事で学んだこと
- Spring Data JPAの基本概念: リポジトリパターンによるデータアクセスの抽象化
- エンティティクラスの設計: データベーステーブルとJavaクラスのマッピング
- リポジトリインターフェースの実装: インターフェースの定義だけでCRUD操作を実現
- クエリメソッドの命名規則: メソッド名からSQLを自動生成する仕組み
- 基本的なCRUD操作の実装: サービス層とコントローラ層の実装
Spring Data JPAを利用することで、データベース操作に関するコードを大幅に削減し、ビジネスロジックの実装により集中できるようになります。
Views: 0