前回はSpring MVCを使った実践的なWebアプリケーション構築について解説しました。今回は、エンタープライズアプリケーション1に不可欠なデータアクセス層の実装について、Spring Data JPAの基礎を中心に解説します。
Spring Data JPAとは
Spring Data JPAは、JPA2ベースのリポジトリ層3を簡単に実装するためのフレームワークです。ボイラープレートコード4を大幅に削減し、データアクセス層の実装を効率化します。
データベースを使うアプリケーションでは、データの保存や取得のために多くのコードを書く必要があります。Spring Data JPAは、これらの定型的なコードを自動的に生成してくれるため、開発者は「商品を保存する」「IDで商品を探す」といった基本的な操作のコードを自分で書く必要がなくなります。
主な特徴
- リポジトリの自動実装: インターフェースを定義するだけで基本的なCRUD5操作が可能
-
メソッド名からクエリを自動生成:
findByName
のようなメソッド名からSQL6を生成 - ページネーション7・ソート機能: 大量データの効率的な処理
-
カスタムクエリのサポート: 複雑なクエリも
@Query
アノテーションで定義可能
プロジェクトのセットアップ
まず、Spring Data JPAを使うために必要な依存関係を追加します。
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- H2 Database (開発・テスト用) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Starter Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
アプリケーション設定
# 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 List<Product> 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 JpaRepository<Product, 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 = ?
Optional<Product> findByName(String name);
// 商品名に特定の文字列を含む商品を検索
// 例:findByNameContaining("パン") → "パン"を含む商品を検索
// SQL: SELECT * FROM products WHERE name LIKE '%パン%'
List<Product> findByNameContaining(String keyword);
// 価格範囲で検索
// SQL: SELECT * FROM products WHERE price >= ? AND price <= ?
List<Product> findByPriceBetween(Integer minPrice, Integer maxPrice);
// 在庫がある商品を価格の安い順で取得
// SQL: SELECT * FROM products WHERE stock_quantity > 0 ORDER BY price ASC
List<Product> findByStockQuantityGreaterThanOrderByPriceAsc(Integer stockQuantity);
// カテゴリIDで商品を検索
// SQL: SELECT * FROM products WHERE category_id = ?
List<Product> 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 JpaRepository<Category, Long> {
// カテゴリ名で検索(一意制約があるため、Optionalで返す)
Optional<Category> findByName(String name);
}
クエリメソッドの命名規則
Spring Data JPAでは、メソッド名から自動的にSQLを生成します。この命名規則を理解すると、様々な検索条件を簡単に実装できます。
よく使うクエリメソッドのパターン
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// ===== 基本的な検索 =====
List<Product> findByName(String name); // 完全一致
List<Product> findByNameIgnoreCase(String name); // 大文字小文字を無視
// ===== 部分一致検索 =====
List<Product> findByNameContaining(String keyword); // 部分一致
List<Product> findByNameStartingWith(String prefix); // 前方一致
List<Product> findByNameEndingWith(String suffix); // 後方一致
// ===== 比較演算子 =====
List<Product> findByPriceLessThan(Integer price); // より小さい
List<Product> findByPriceLessThanEqual(Integer price); // 以下
List<Product> findByPriceGreaterThan(Integer price); // より大きい
List<Product> findByPriceGreaterThanEqual(Integer price); // 以上
// ===== 複数条件 =====
List<Product> findByNameAndPrice(String name, Integer price); // AND条件
List<Product> findByNameOrPrice(String name, Integer price); // OR条件
// ===== NULL チェック =====
List<Product> findByDescriptionIsNull(); // NULLのもの
List<Product> findByDescriptionIsNotNull(); // NULLでないもの
// ===== IN句 =====
List<Product> findByNameIn(List<String> names); // 複数の値のいずれか
// ===== ソート =====
List<Product> findByOrderByPriceAsc(); // 価格の昇順
List<Product> findByOrderByPriceDesc(); // 価格の降順
// ===== 複雑な条件 =====
List<Product> 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 List<Product> 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 List<Product> searchProductsByName(String keyword) {
return productRepository.findByNameContaining(keyword);
}
// 価格範囲で検索
@Transactional(readOnly = true)
public List<Product> 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 ResponseEntity<Product> 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 List<Product> getAllProducts() {
return productService.getAllProducts();
}
// 商品をID検索
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.getProductById(id);
}
// 商品名で検索
@GetMapping("/search")
public List<Product> searchProducts(@RequestParam String keyword) {
return productService.searchProductsByName(keyword);
}
// 価格範囲で検索
@GetMapping("/search/price")
public List<Product> 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を利用することで、データベース操作に関するコードを大幅に削減し、ビジネスロジックの実装により集中できるようになります。
-
エンタープライズアプリケーション - 企業の業務システムなど、高い信頼性・拡張性・保守性が求められる大規模なアプリケーション。 ↩
-
JPA (Java Persistence API) - JavaでO/Rマッピング(オブジェクト関係マッピング)を実現するための標準API。HibernateはJPAの代表的な実装。 ↩
-
リポジトリ層 - データアクセスを担当する層。データの永続化と復元を担当し、ビジネスロジックからデータアクセスの詳細を隠蔽する。 ↩
-
ボイラープレートコード - 定型的で繰り返し記述が必要なコード。Spring Data JPAはこれらを自動生成することで開発効率を向上させる。 ↩
-
CRUD操作 - Create(作成)、Read(読取)、Update(更新)、Delete(削除)の基本的なデータ操作の総称。 ↩
-
SQL (Structured Query Language) - リレーショナルデータベースを操作するための標準的なクエリ言語。 ↩
-
ページネーション - 大量のデータを一定数ずつに分割して表示する仕組み。パフォーマンスとユーザビリティの向上に寄与。 ↩
-
エンティティ - データベースのテーブルに対応するJavaオブジェクト。
@Entity
アノテーションで定義される。 ↩ -
楽観的ロック - データ更新時にバージョン番号を確認することで、他のトランザクションによる更新を検出する仕組み。 ↩
-
監査機能 - データの作成日時、更新日時などを自動的に記録する機能。 ↩
-
フェッチ戦略 - 関連エンティティをいつ取得するかを決定する戦略。EAGER(即時取得)とLAZY(遅延取得)がある。 ↩
-
Lombok - Javaのボイラープレートコードを削減するライブラリ。
@Getter
、@Setter
などのアノテーションでコードを自動生成。 ↩