Spring Boot Repository の基本と実践:サンプルコード付き
はじめに
現代のアプリケーション開発において、データベースとの連携は不可欠です。データの永続化、検索、更新、削除といった操作は、ほとんどすべての業務アプリケーションの中核を成します。しかし、これらのデータアクセス処理をアプリケーションのビジネスロジックと混在させると、コードの可読性や保守性が著しく低下します。
ここで重要になるのが「データアクセス層の分離」です。アプリケーション設計におけるデザインパターンの一つであるDAO (Data Access Object) パターンやRepositoryパターンは、データアクセスに関するロジックを独立したモジュールとして定義することを推奨します。これにより、ビジネスロジックはデータ永続化の詳細から解放され、よりシンプルでテストしやすいものになります。
Spring Frameworkは、このデータアクセス層の実装を強力に支援する機能を提供しています。特に、Spring Bootと共に使用されることが多い「Spring Data」プロジェクトは、リポジトリの実装を驚くほど簡単にします。開発者は、インターフェースを定義するだけで、一般的なCRUD (Create, Read, Update, Delete) 操作や、さらには複雑なクエリ実行メソッドまで、自動的に実装される恩恵を受けることができます。
この記事では、Spring BootにおけるRepositoryの基本的な概念から、Spring Data JPAを中心とした実践的な使い方までを詳細に解説します。基本的なCRUD操作、クエリメソッド、@Queryアノテーションによるカスタムクエリ、トランザクション管理、ページング、ソートといった機能について、具体的なサンプルコードと共に学んでいきます。
データアクセス層の実装に悩んでいる方、Spring Dataの強力な機能を活用したい方にとって、この記事が役立つ情報源となることを願います。
Repositoryとは
アプリケーションにおいて、Repository(リポジトリ)は、ドメインオブジェクト(ビジネスロジックで扱う対象となるデータ)の永続化された集合へのアクセスを管理する役割を担います。データベースのような永続化メカニズムの詳細を隠蔽し、コレクションのようなインターフェースを提供することで、ビジネスロジック層からデータアクセス層への依存度を下げます。
簡単に言えば、Repositoryは「データを保存したり、探し出したりするための窓口」のようなものです。ビジネスロジックはRepositoryインターフェースを通じてデータにアクセスし、そのデータがリレーショナルデータベースに保存されているのか、NoSQLデータベースなのか、あるいはファイルシステムなのかといった実装の詳細を気にする必要がありません。
Spring Dataプロジェクト
Spring Frameworkは、様々なデータストアに対応したデータアクセス層を効率的に開発するための包括的なプロジェクト群を提供しており、これを「Spring Data」と呼びます。Spring Dataは、各データストア(リレーショナルデータベース、MongoDB、Redis、Elasticsearchなど)に対して、共通のプログラミングモデルを提供することを目指しています。
Spring Dataの主要な特徴は以下の通りです。
- Repositoryインターフェースの自動実装: 開発者はRepositoryインターフェースを定義するだけで、Spring Dataがそのインターフェースの実装クラスを自動的に生成します。
- CRUD機能の提供: 標準的なRepositoryインターフェース(
CrudRepositoryなど)を継承するだけで、基本的な保存、検索、更新、削除機能を利用できます。 - クエリメソッドの自動生成: メソッド名に特定の命名規則を用いることで、Spring Dataが自動的に対応するクエリを生成し実行します。
- カスタムクエリのサポート:
@Queryアノテーションを使用して、JPQLやNative SQLによるカスタムクエリを定義できます。 - ページングとソートの容易な実現: 大量のデータを扱う際に便利なページング(ページ分割)とソート(並び替え)機能が簡単に利用できます。
この記事では、リレーショナルデータベースへのアクセスに広く利用される「Spring Data JPA」を中心に解説を進めます。
JPA (Java Persistence API) の概要
Spring Data JPAは、その名の通り、Java Persistence API (JPA) をベースにしています。JPAは、Javaにおけるオブジェクトリレーショナルマッピング (ORM) の標準仕様です。ORMとは、オブジェクト指向プログラミング言語とリレーショナルデータベースの間にある概念の隔たり(インピーダンスミスマッチ)を解決するための技術です。JPAを使用することで、開発者はデータベースのテーブルをJavaのクラス(エンティティ)として扱い、SQLを書く代わりにオブジェクトの操作としてデータ永続化処理を記述できます。
JPAは仕様であり、その実装としてはHibernate, EclipseLink, Apache OpenJPAなどがあります。Spring Bootのスターターを使用する場合、通常はHibernateがデフォルトのJPAプロバイダーとして利用されます。
Spring Data JPAは、このJPAの上にさらに抽象化レイヤーを提供し、JPAを使用したデータアクセス開発をさらに効率化します。
@Repositoryアノテーションの役割
Spring Frameworkでは、データアクセスオブジェクトに対して@Repositoryアノテーションを付与することが推奨されていました。このアノテーションは、SpringのDIコンテナに対して、そのクラスがデータアクセスに関する役割を持つことを示し、JDBC関連の例外をSpring独自のデータアクセス例外階層に自動的に変換する機能を提供します。
しかし、Spring Dataを利用する場合、開発者が作成するのはRepositoryインターフェースであり、その実装はSpring Dataが自動生成します。そして、Spring Dataが生成する実装クラスは、自動的にSpring Beanとして登録され、例外変換も適切に行われます。
したがって、Spring DataのRepositoryインターフェースに対しては、通常@Repositoryアノテーションを明示的に付ける必要はありません。 Spring Dataがインターフェースをスキャンして自動的にBeanとして登録してくれるからです。ただし、カスタム実装クラスを作成し、それをSpring Beanとして登録したい場合などには使用することがあります。基本的なSpring Dataの利用においては、インターフェース定義のみで十分です。
Spring Data JPAの基本的なCRUD操作
Spring Data JPAを使用する最初のステップは、データベーステーブルに対応するエンティティクラスと、そのエンティティに対するデータアクセス操作を定義するRepositoryインターフェースを作成することです。
エンティティクラスの作成
JPAエンティティは、@Entityアノテーションをクラスに付与することで定義します。主キーには@Idを付与し、主キーの生成方法を指定するために@GeneratedValueを使用します。
例として、簡単な商品情報を表すProductエンティティを作成します。
“`java
// src/main/java/com/example/demo/entity/Product.java
package com.example.demo.entity;
import javax.persistence.; // jakarta.persistence.; if using Spring Boot 3+
@Entity
@Table(name = “products”) // テーブル名を明示的に指定する場合
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // データベースのIDENTITY列を使用
private Long id;
private String name;
private double price;
private int stock;
// デフォルトコンストラクタはJPAがエンティティを生成する際に必要
public Product() {
}
public Product(String name, double price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
// GetterとSetter
public Long getId() {
return id;
}
// IDのSetterは通常は不要(データベースが生成するため)
// public void setId(Long id) { this.id = id; }
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public int getStock() {
return stock;
}
public void setStock(int stock) {
this.stock = stock;
}
@Override
public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
", price=" + price +
", stock=" + stock +
'}';
}
}
“`
このクラスは、productsテーブルの各行に対応します。@Idと@GeneratedValueにより、idフィールドが主キーであり、新しいエンティティが保存される際にデータベースによって自動的に値が生成されるように設定しています。
Repositoryインターフェースの作成
次に、このProductエンティティに対するRepositoryインターフェースを作成します。Spring Data JPAでは、CrudRepository, PagingAndSortingRepository, JpaRepositoryといった基本的なインターフェースが提供されています。これらを継承することで、様々な標準的なデータアクセス操作メソッドを利用できるようになります。
CrudRepository<T, ID>: 最も基本的なインターフェースで、CRUD操作(保存、IDによる検索、全件検索、IDによる削除、エンティティによる削除など)を提供します。Tはエンティティの型、IDは主キーの型を指定します。PagingAndSortingRepository<T, ID>:CrudRepositoryを継承しており、さらにページングとソート機能を追加で提供します。JpaRepository<T, ID>:PagingAndSortingRepositoryを継承しており、JPA固有の機能(フラッシュ、バッチ削除など)を追加で提供します。通常、Spring Data JPAを使用する際には、このJpaRepositoryを継承するのが一般的です。
Productエンティティに対するRepositoryは以下のようになります。
“`java
// src/main/java/com/example/demo/repository/ProductRepository.java
package com.example.demo.repository;
import com.example.demo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
// import org.springframework.data.repository.CrudRepository; // または CrudRepository
// import org.springframework.data.repository.PagingAndSortingRepository; // または PagingAndSortingRepository
// JpaRepository<エンティティの型, 主キーの型> を継承する
public interface ProductRepository extends JpaRepository
// これだけで、基本的なCRUD操作メソッドが利用可能になる
// findById(ID id), findAll(), save(T entity), deleteById(ID id), delete(T entity) など
}
“`
これで、ProductRepositoryインターフェースをDIコンテナから注入して使用できるようになります。Spring Bootの自動設定により、アプリケーション起動時にこのインターフェースに対する実装クラスが自動的に生成され、Spring Beanとして登録されます。
基本的なCRUD操作の実行
Repositoryインターフェースが定義できたら、それを他のSpring Bean(例えばServiceクラスやControllerクラス)に注入して使用できます。
“`java
// src/main/java/com/example/demo/service/ProductService.java
package com.example.demo.service;
import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional // Service層でトランザクション管理を行うのが一般的
public class ProductService {
private final ProductRepository productRepository;
@Autowired // コンストラクタインジェクション
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// 作成 (Create) / 更新 (Update)
public Product saveProduct(Product product) {
// idがnullの場合は新規作成、idが存在する場合は更新
return productRepository.save(product);
}
// 検索 (Read) - 全件取得
@Transactional(readOnly = true) // 読み取り専用トランザクション
public List<Product> getAllProducts() {
return productRepository.findAll();
}
// 検索 (Read) - IDによる取得
@Transactional(readOnly = true)
public Optional<Product> getProductById(Long id) {
// findByIdはOptional<T>を返す
return productRepository.findById(id);
}
// 削除 (Delete) - IDによる削除
public void deleteProductById(Long id) {
productRepository.deleteById(id);
}
// 削除 (Delete) - エンティティによる削除
public void deleteProduct(Product product) {
productRepository.delete(product);
}
// その他のCrudRepositoryメソッド例:
@Transactional(readOnly = true)
public long countProducts() {
return productRepository.count(); // 総数を取得
}
public void deleteAllProducts() {
productRepository.deleteAll(); // 全件削除
}
}
“`
このProductServiceクラスでは、@Autowiredアノテーション(またはコンストラクタインジェクション)によってProductRepositoryが注入されています。そして、Repositoryが提供するsave, findAll, findById, deleteById, deleteといったメソッドを使って、CRUD操作を実行しています。
サンプルコードの実行環境:
これらのコードを実行するためには、Spring Bootプロジェクトが必要です。以下のステップで簡単に構築できます。
- Spring Initializr (
https://start.spring.io/) でプロジェクトを作成します。- Project: Maven/Gradle
- Language: Java
- Spring Boot version: 安定版
- Dependencies:
Spring Web,Spring Data JPA,H2 Database(開発/テスト用インメモリデータベース) または他のデータベースドライバ (MySQL Driver,PostgreSQL Driverなど)
- プロジェクトをダウンロードし、IDEで開きます。
-
application.propertiesまたはapplication.ymlにデータベース設定を追加します(H2の場合はデフォルトでほとんど設定不要ですが、コンソール有効化など)。“`properties
application.properties
spring.jpa.hibernate.ddl-auto=update # or create, create-drop, none
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
“`
4. 上記のエンティティ、Repository、Serviceクラスをそれぞれのパッケージに配置します。
5. アプリケーションを起動し、動作を確認します。例えば、Spring Bootのコマンドラインランナーや簡単なRESTコントローラーを作成してServiceメソッドを呼び出すことができます。
“`java
// src/main/java/com/example/demo/DemoApplication.java
package com.example.demo;
import com.example.demo.entity.Product;
import com.example.demo.service.ProductService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public CommandLineRunner demo(ProductService productService) {
return (args) -> {
// 初期データの投入
productService.saveProduct(new Product("Laptop", 1200.00, 10));
productService.saveProduct(new Product("Mouse", 25.00, 50));
productService.saveProduct(new Product("Keyboard", 75.00, 30));
// 全件検索
System.out.println("--- Products found with findAll():");
productService.getAllProducts().forEach(System.out::println);
System.out.println("");
// IDで検索
productService.getProductById(1L)
.ifPresent(product -> System.out.println("Product found with findById(1L): " + product));
System.out.println("");
// ID=2の製品を更新
productService.getProductById(2L)
.ifPresent(product -> {
product.setPrice(20.00); // 値下げ
productService.saveProduct(product);
System.out.println("Updated product (ID 2): " + product);
});
System.out.println("");
// ID=3の製品を削除
System.out.println("Deleting product with ID 3...");
productService.deleteProductById(3L);
// 再度全件検索して確認
System.out.println("--- Products found after deletion:");
productService.getAllProducts().forEach(System.out::println);
System.out.println("");
System.out.println("Total products count: " + productService.countProducts());
};
}
}
“`
このCommandLineRunnerは、アプリケーション起動後に実行され、Repositoryを通じていくつかのCRUD操作を行います。H2データベースを使用している場合、アプリケーションを停止するとデータは失われます。永続化したい場合は、ファイルベースのH2データベースや他のデータベースを使用してください。
クエリメソッド (Derived Query Methods)
Spring Dataの最も強力で便利な機能の一つが、クエリメソッドです。これは、「特定の命名規則に従ってRepositoryインターフェースにメソッドを定義するだけで、Spring Dataがそのメソッド名から自動的にクエリを生成してくれる」というものです。
メソッド名は、find...By, count...By, delete...By, exists...Byといったキーワードで始まり、その後ろにエンティティのプロパティ名と条件を示すキーワードを組み合わせる形で記述します。
基本的なクエリメソッドの例
ProductRepositoryにいくつかのクエリメソッドを追加してみましょう。
“`java
// src/main/java/com/example/demo/repository/ProductRepository.java (修正版)
package com.example.demo.repository;
import com.example.demo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import java.util.Optional;
public interface ProductRepository extends JpaRepository
// nameで商品を検索
List<Product> findByName(String name);
// nameとpriceで商品を検索
List<Product> findByNameAndPrice(String name, double price);
// priceの範囲で商品を検索
List<Product> findByPriceBetween(double minPrice, double maxPrice);
// stockが指定値より少ない商品を検索
List<Product> findByStockLessThan(int maxStock);
// nameで始まる商品を検索 (LIKE '...')
List<Product> findByNameStartingWith(String prefix);
// nameが含まれる商品を検索 (LIKE '%...')
List<Product> findByNameContaining(String part);
// nameで終わる商品を検索 (LIKE '...%')
List<Product> findByNameEndingWith(String suffix);
// priceが指定値より大きいか等しい商品を検索
List<Product> findByPriceGreaterThanEqual(double minPrice);
// activeがtrueの商品を検索 (boolean型)
// Предпо假设 Productエンティティに boolean active; フィールドがあるとします。
// List<Product> findByActiveTrue();
// 名前で検索し、結果をpriceの降順でソート
List<Product> findByNameOrderByPriceDesc(String name);
// 在庫が指定値より少ない商品を検索し、上位10件を取得
List<Product> findTop10ByStockLessThan(int maxStock);
// 名前で商品を検索し、最初の1件を取得
Optional<Product> findFirstByName(String name); // findTopByName と同じ
// 指定されたIDのリストに含まれる商品を検索
List<Product> findByIdIn(List<Long> ids);
// 在庫が指定値以下の商品をカウント
long countByStockLessThanEqual(int maxStock);
// 指定された名前の商品が存在するか確認
boolean existsByName(String name);
// 指定された名前の商品を削除
// 戻り値は削除されたレコード数
long deleteByName(String name);
// priceが指定値より大きい商品をページングして取得
Page<Product> findByPriceGreaterThan(double minPrice, Pageable pageable);
// priceが指定値より大きい商品をソートして取得
List<Product> findByPriceGreaterThan(double minPrice, Sort sort);
}
“`
これらのメソッド名を見れば、どのようなクエリが実行されるか直感的に理解できます。Spring Data JPAは、これらのメソッド名を解析し、対応するJPQLクエリを自動生成します。
クエリメソッドの命名規則:
基本的には find...By<条件プロパティ><条件キーワード><結合子><次の条件プロパティ>...<OrderByプロパティ><ソート方向> の形式に従います。
- キーワード:
And,Or,Is,Equals,Between,LessThan,LessThanEqual,GreaterThan,GreaterThanEqual,After,Before,IsNull,IsNotNull,Like,NotLike,StartingWith,EndingWith,Containing,In,NotIn,True,False,IgnoreCase,OrderBy,Limit,Top,First,Count,Delete,Existsなど多数。 - プロパティ: エンティティクラスのフィールド名(キャメルケース)を、メソッド名ではパスカルケース(最初の文字を大文字)にします。例えば、
nameフィールドならメソッド名ではNameとなります。関連エンティティのプロパティを指定することも可能です(例:findByCategoryName(String categoryName))。
利用例:
“`java
// ProductService.java (メソッド追加)
// … (既存コード) …
@Service
@Transactional
public class ProductService {
// ... (既存の注入とメソッド) ...
@Transactional(readOnly = true)
public List<Product> getProductsByName(String name) {
return productRepository.findByName(name);
}
@Transactional(readOnly = true)
public List<Product> getProductsByPriceRange(double minPrice, double maxPrice) {
return productRepository.findByPriceBetween(minPrice, maxPrice);
}
@Transactional(readOnly = true)
public List<Product> getLowStockProducts(int maxStock) {
return productRepository.findByStockLessThan(maxStock);
}
@Transactional(readOnly = true)
public List<Product> searchProductsByNameContaining(String part) {
return productRepository.findByNameContaining(part);
}
@Transactional(readOnly = true)
public List<Product> searchAndSortProductsByName(String name) {
// findByNameOrderByPriceDesc メソッドは引数にnameが必要です。
// findByNameOrderByPriceDesc は List<Product> findByNameOrderByPriceDesc(String name); です。
// findByNameOrderByPriceDesc は name に一致するものを price desc でソートします。
// もし name を条件に含めず、全ての製品を price desc でソートしたい場合は findAll(Sort sort) を使います。
// List<Product> sortedProducts = productRepository.findAll(Sort.by(Sort.Direction.DESC, "price"));
// 例として name を条件にしたソートメソッドを使います
return productRepository.findByNameOrderByPriceDesc(name);
}
@Transactional(readOnly = true)
public List<Product> getTop10LowStockProducts(int maxStock) {
return productRepository.findTop10ByStockLessThan(maxStock);
}
@Transactional
public long deleteProductsByName(String name) {
// deleteBy... メソッドはデフォルトでは @Transactional が必要
return productRepository.deleteByName(name);
}
// ...
}
“`
クエリメソッドは、比較的単純なクエリに対して非常に生産性が高いです。しかし、メソッド名が長くなりすぎたり、複雑な条件(サブクエリなど)が必要な場合には、後述の@Queryアノテーションを使用するか、SpecificationやQuerydslといったより強力なツールを検討する必要があります。
JPQL/Native SQLクエリ (@Query)
クエリメソッドでは表現できない、より複雑なクエリを実行したい場合や、既存のクエリをそのまま利用したい場合には、@Queryアノテーションを使用します。@Queryを使用すると、Repositoryインターフェースのメソッドに対して、実行したいJPQL (Java Persistence Query Language) クエリまたはNative SQLクエリを直接指定できます。
JPQLクエリ
JPQLは、JPAエンティティとそれらの関連に対してクエリを実行するためのオブジェクト指向クエリ言語です。SQLと似ていますが、テーブル名やカラム名ではなく、エンティティ名やフィールド名を使用します。
@QueryアノテーションにJPQLクエリ文字列を指定します。
“`java
// src/main/java/com/example/demo/repository/ProductRepository.java (さらに修正版)
package com.example.demo.repository;
import com.example.demo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
// … (既存のimportとインターフェース定義) …
public interface ProductRepository extends JpaRepository
// ... (既存のクエリメソッド) ...
// JPQLによるカスタムクエリ
// 名前の一部を含む商品を検索 (LIKE句)
@Query("SELECT p FROM Product p WHERE p.name LIKE %:name%")
List<Product> searchByNameLike(@Param("name") String name); // @Paramでパラメータ名を指定
// 指定した価格以上の商品を在庫昇順で取得
@Query("SELECT p FROM Product p WHERE p.price >= ?1 ORDER BY p.stock ASC")
List<Product> findByPriceGreaterThanOrderedByStock(double minPrice); // パラメータは ?1, ?2, ... で指定
// 在庫がゼロの商品を検索
@Query("SELECT p FROM Product p WHERE p.stock = 0")
List<Product> findOutOfStockProducts();
// 特定の列のみを選択して取得(DTOとしてマッピングする場合など)
// 例: 商品名と価格だけを取得 (Object配列として返される)
@Query("SELECT p.name, p.price FROM Product p")
List<Object[]> findProductNameAndPrice();
// または、特定のコンストラクタを持つDTOにマッピング
// 例: ProductInfoというDTO (String name, double price) がある場合
// @Query("SELECT new com.example.demo.dto.ProductInfo(p.name, p.price) FROM Product p")
// List<ProductInfo> findProductInfo();
}
“`
パラメータの扱い:
- 名前付きパラメータ:
@Param("paramName")アノテーションをメソッド引数に付与し、クエリ内で:paramNameの形式で使用するのが推奨される方法です。可読性が高く、引数の順序に依存しません。 - 位置パラメータ: クエリ内で
?1,?2, … の形式で使用し、メソッド引数の順序に対応させます。引数の順序を変更するとクエリも修正する必要があるため、名前付きパラメータより推奨されません。
Native SQLクエリ
データベース固有のSQL機能を利用したい場合や、既存の複雑なSQLクエリをそのまま利用したい場合には、Native SQLクエリを実行できます。@QueryアノテーションのnativeQuery属性をtrueに設定します。
“`java
// src/main/java/com/example/demo/repository/ProductRepository.java (さらに修正版)
package com.example.demo.repository;
import com.example.demo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
// … (既存のimportとインターフェース定義) …
public interface ProductRepository extends JpaRepository
// ... (既存のJPQLクエリなど) ...
// Native SQLによるカスタムクエリ
// 在庫が指定値と等しい商品を検索
@Query(value = "SELECT * FROM products WHERE stock = :stock", nativeQuery = true)
List<Product> findByStockNative(@Param("stock") int stock);
// ネイティブクエリで特定の列のみを取得
@Query(value = "SELECT name, price FROM products WHERE id = :id", nativeQuery = true)
List<Object[]> findProductNameAndPriceNative(@Param("id") Long id);
// ネイティブクエリで集計関数を使用
@Query(value = "SELECT AVG(price) FROM products", nativeQuery = true)
Double getAveragePriceNative();
}
“`
Native SQLを使用する場合、データベースの種類に依存する可能性があるため、移植性には注意が必要です。
更新クエリ (@Modifying)
INSERT, UPDATE, DELETE といったデータ変更を行うJPQLやNative SQLクエリを実行するメソッドには、@Modifyingアノテーションを付与する必要があります。また、これらの操作はトランザクション内で実行される必要があるため、通常は@Transactionalアノテーションも必要です。
“`java
// src/main/java/com/example/demo/repository/ProductRepository.java (さらに修正版)
package com.example.demo.repository;
import com.example.demo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional; // トランザクション関連のimport
import java.util.List;
// … (既存のimportとインターフェース定義) …
public interface ProductRepository extends JpaRepository
// ... (既存のJPQL/Native SQLクエリなど) ...
// 在庫数を減らす更新クエリ
@Modifying // 更新クエリであることを示す
@Transactional // トランザクション内で実行されるようにする
@Query("UPDATE Product p SET p.stock = p.stock - :amount WHERE p.id = :id")
int decreaseStock(@Param("id") Long id, @Param("amount") int amount); // 戻り値は更新されたレコード数
// 特定の価格以下の商品を削除するクエリ
@Modifying
@Transactional
@Query("DELETE FROM Product p WHERE p.price < :price")
int deleteProductsByPriceLessThan(@Param("price") double price); // 戻り値は削除されたレコード数
}
“`
@Modifyingアノテーションが付与されたメソッドは、クエリ実行後にJPAの永続化コンテキストをクリア(または特定のエンティティをデタッチ)する必要がある場合があります。これは、永続化コンテキスト内のエンティティの状態がデータベースの実際の状態と一致しなくなる可能性があるためです。@Modifying(clearAutomatically = true) と設定すると、クエリ実行後に永続化コンテキストが自動的にクリアされます。
利用例:
“`java
// ProductService.java (メソッド追加)
// … (既存コード) …
@Service
@Transactional
public class ProductService {
// ... (既存の注入とメソッド) ...
@Transactional(readOnly = true)
public List<Product> findProductsByNameLike(String namePart) {
return productRepository.searchByNameLike(namePart);
}
@Transactional(readOnly = true)
public List<Product> findProductsByStockNative(int stock) {
return productRepository.findByStockNative(stock);
}
@Transactional
public int decreaseProductStock(Long productId, int amount) {
// 在庫を減らす更新クエリを実行
int updatedCount = productRepository.decreaseStock(productId, amount);
if (updatedCount > 0) {
System.out.println("Decreased stock for product ID " + productId + " by " + amount + ". Updated count: " + updatedCount);
} else {
System.out.println("Product ID " + productId + " not found or stock update failed.");
}
return updatedCount;
}
@Transactional
public int removeProductsBelowPrice(double price) {
// 特定価格以下の製品を削除
int deletedCount = productRepository.deleteProductsByPriceLessThan(price);
System.out.println("Deleted " + deletedCount + " products with price less than " + price);
return deletedCount;
}
// ...
}
“`
@Queryアノテーションは、クエリメソッドでは表現しきれない複雑な検索条件や、データベースの機能(特定の関数など)を利用したい場合に非常に有用です。
トランザクション管理
データベース操作において、複数の処理を一つの論理的な単位として扱う必要がある場合があります。例えば、「商品の在庫を減らし、注文履歴を作成する」という一連の操作は、どちらか一方だけが成功して他方が失敗するとデータの整合性が崩れてしまいます。このような場合に利用されるのがトランザクションです。
トランザクションは、以下の4つの性質(ACID特性)を持ちます。
- Atomicity (原子性): トランザクション内の全ての操作は、完全に実行されるか、全く実行されないかのどちらかです。部分的な完了はありません。
- Consistency (一貫性): トランザクションの開始時と終了時において、データベースの整合性が保たれます。
- Isolation (独立性): 複数のトランザクションが同時に実行されても、それぞれが他のトランザクションの影響を受けないように見えます。
- Durability (耐久性): トランザクションが一度コミットされたら、システム障害が発生しても、その変更は永続的に保持されます。
Spring Frameworkは、宣言的トランザクション管理という非常に便利な機能を提供しています。これは、アノテーションやXML設定を使用することで、コード内にトランザクション管理ロジック(beginTransaction(), commit(), rollback()など)を記述することなく、メソッド単位でトランザクションを適用できる機能です。
Spring BootでSpring Data JPAを使用する場合、spring-boot-starter-data-jpaスターターにspring-tx(Spring Transaction)が含まれているため、特別な設定なしに@Transactionalアノテーションを利用できます。
@Transactionalアノテーション
トランザクション管理を行いたいメソッドやクラスに@Transactionalアノテーションを付与します。
- クラスに付与: クラス内の全てのpublicメソッドにトランザクションが適用されます。
- メソッドに付与: その特定のメソッドにのみトランザクションが適用されます。クラスとメソッドの両方に付与された場合は、メソッドのアノテーションが優先されます。
Spring Data Repositoryメソッドとトランザクション:
Spring Dataが自動生成するRepositoryの実装クラスは、デフォルトでほとんどのメソッド(save, delete, deleteAll, saveAllなど変更系メソッド)に対してトランザクションが適用されています。find...系の読み取り専用メソッドは、通常は読み取り専用トランザクションとして実行されます(または、トランザクションなしで実行される設定もあります)。
ただし、これはRepositoryの「自動生成された」メソッドに対する挙動です。開発者がRepositoryインターフェースに独自に追加したメソッド(クエリメソッドや@Queryメソッド)については、デフォルトではトランザクション管理の対象になりません。特にデータ変更を行う @Modifying 付きの @Query メソッドや、複数のRepositoryメソッドを呼び出すようなビジネスロジックは、通常Service層でトランザクションを管理する必要があります。
Service層でのトランザクション管理:
一般的なアプリケーション構造では、Repositoryは単一のエンティティや集計根に対するCRUD操作に責任を持ち、複数のRepository操作や複雑なビジネスロジックはService層に配置します。したがって、複数の永続化操作を含むServiceメソッドに対して@Transactionalアノテーションを付与するのが推奨されるプラクティスです。
例: 注文処理
「注文を受け付け、在庫を減らし、注文情報を保存する」という処理を考えます。この処理は、全て成功するか全て失敗するかのいずれかであるべきです。
“`java
// src/main/java/com/example/demo/service/OrderService.java
package com.example.demo.service;
import com.example.demo.entity.Product;
// import com.example.demo.entity.Order; // 注文エンティティがあると仮定
// import com.example.demo.repository.OrderRepository; // 注文リポジトリがあると仮定
import com.example.demo.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
public class OrderService {
private final ProductRepository productRepository;
// private final OrderRepository orderRepository; // 注文リポジトリ
@Autowired
public OrderService(ProductRepository productRepository /*, OrderRepository orderRepository*/) {
this.productRepository = productRepository;
// this.orderRepository = orderRepository;
}
@Transactional // このメソッド全体がトランザクションの対象となる
public boolean placeOrder(Long productId, int quantity) {
Optional<Product> productOpt = productRepository.findById(productId);
if (!productOpt.isPresent()) {
System.out.println("Product not found: " + productId);
return false; // 製品が見つからない場合は失敗
}
Product product = productOpt.get();
if (product.getStock() < quantity) {
System.out.println("Insufficient stock for product " + product.getName() + ": " + product.getStock());
return false; // 在庫不足の場合は失敗
}
// 在庫を減らす
// product.setStock(product.getStock() - quantity);
// productRepository.save(product); // エンティティを更新
// あるいは、Repositoryの更新クエリを使う
// decreaseStock(@Param("id") Long id, @Param("amount") int amount) メソッドを使用
int updatedRows = productRepository.decreaseStock(productId, quantity);
if (updatedRows == 0) {
// ここに来るべきではないが、念のためチェック
System.out.println("Failed to decrease stock for product ID " + productId);
// RuntimeExceptionをスローするとトランザクションがロールバックされる
throw new RuntimeException("Stock update failed");
}
// TODO: 注文情報を保存する処理 (別のリポジトリが必要)
// Order order = new Order(productId, quantity, ...);
// orderRepository.save(order);
System.out.println("Order placed successfully for product " + product.getName() + ", quantity " + quantity);
// ここでメソッドが正常終了すると、トランザクションがコミットされる
return true;
}
// 例: エラーが発生してトランザクションがロールバックされるケース
@Transactional
public void performAtomicUpdateWithPotentialError(Long productId, int quantity) {
// 在庫を減らす
// ... (decrease stock logic) ...
productRepository.decreaseStock(productId, quantity);
// TODO: 注文情報を保存する処理
// orderRepository.save(order);
// 例外を発生させる
if (true) { // 何らかの条件
System.out.println("Simulating an error to trigger rollback...");
throw new RuntimeException("Something went wrong during order processing!");
}
// この後のコードは実行されない
System.out.println("This line will not be printed if an exception is thrown.");
}
}
“`
@Transactionalアノテーションが付与されたplaceOrderメソッド内で例外(デフォルトでは非チェック例外であるRuntimeExceptionやそのサブクラス)がスローされると、Springのトランザクションマネージャーはそのトランザクションをロールバックし、メソッド内で実行された全てのデータベース操作は取り消されます。チェック例外(Exceptionやそのサブクラス、ただしRuntimeExceptionのサブクラスを除く)は、デフォルトではロールバックのトリガーになりません。この挙動は@Transactional(rollbackFor = Exception.class)のように設定で変更できます。
トランザクションの属性
@Transactionalアノテーションには、トランザクションの挙動を制御するための様々な属性があります。
propagation: トランザクションの伝播設定。あるトランザクションコンテキスト内で、別のトランザクションメソッドが呼び出された場合に、どのようにトランザクションを扱うかを定義します。REQUIRED(デフォルト): トランザクションが進行中であればそれに参加し、進行中でなければ新しいトランザクションを開始します。REQUIRES_NEW: 常に新しいトランザクションを開始し、進行中のトランザクションがあれば一時停止します。SUPPORTS: トランザクションが進行中であればそれに参加し、進行中でなければトランザクションなしで実行します。NOT_SUPPORTED: トランザクションなしで実行し、進行中のトランザクションがあれば一時停止します。MANDATORY: トランザクションが進行中である必要があります。進行中でなければ例外をスローします。NEVER: トランザクションが進行中でない必要があります。進行中であれば例外をスローします。NESTED: ネストされたトランザクションを開始します(JDBCではSAVEPOINTを使用)。親トランザクションがロールバックされても、ネストされたトランザクション内の変更は保持される可能性がありますが、親がコミットしない限り子もコミットされません。親のロールバックは子のロールバックを引き起こしますが、子のロールバックは親に影響しません(親がcatchした場合)。通常JPA/Hibernateでは推奨されません。
isolation: トランザクション分離レベル。複数のトランザクションが同時に実行される際に、データにどのような影響を与え合うかを定義します。低いレベルほど並列性は高まりますが、以下の問題が発生する可能性があります。READ_UNCOMMITTED: 他のトランザクションの未コミットの変更を読み込む(ダーティリード)。最も分離レベルが低い。READ_COMMITTED: コミットされた変更のみ読み込む。ダーティリードを防ぐが、反復不能読み込みやファントムリードが発生する可能性がある。多くのデータベースのデフォルト。REPEATABLE_READ: 同じトランザクション内で同じクエリを繰り返した場合、常に同じ結果が得られることを保証する(反復不能読み込みを防ぐ)。ファントムリードは発生する可能性がある。MySQLのデフォルト。SERIALIZABLE: トランザクションを直列に実行したかのように振る舞う(ダーティリード、反復不能読み込み、ファントムリード全てを防ぐ)。最も分離レベルが高いが、並列性が著しく低下する。DEFAULT(デフォルト): 使用しているデータソースのデフォルト分離レベルを使用します。
readOnly:trueに設定すると、そのトランザクションは読み取り専用であることをデータベースドライバやJPAプロバイダーに伝えます。これにより、パフォーマンスの最適化が行われる場合があります。データ変更を行わないメソッドには積極的にreadOnly = trueを設定すべきです。timeout: トランザクションがタイムアウトするまでの秒数を指定します。rollbackFor,rollbackForClassName: 指定した例外クラスまたはクラス名がスローされた場合にトランザクションをロールバックします。デフォルトでは非チェック例外とErrorのみがロールバックの対象です。noRollbackFor,noRollbackForClassName: 指定した例外クラスまたはクラス名がスローされてもトランザクションをロールバックしません。
“`java
@Service
@Transactional(readOnly = true) // クラスレベルで読み取り専用をデフォルトにする
public class ProductService {
// ... (注入など) ...
// データ変更を伴うメソッドは、@TransactionalでreadOnly=false(デフォルト)にする
@Transactional // readOnly = false が適用される
public Product saveProduct(Product product) {
return productRepository.save(product);
}
// 注文処理など、複数の操作をアトミックに行う必要があるメソッド
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {OrderProcessingException.class})
public boolean placeOrderWithCustomException(Long productId, int quantity) throws OrderProcessingException {
// ... (注文ロジック) ...
// OrderProcessingException がスローされた場合にロールバックされる
// RuntimeException や Error はデフォルトでロールバックされる
// OrderProcessingException をチェック例外として定義している場合、rollbackFor で指定しないとロールバックされない
return true; // 成功
}
}
“`
トランザクション管理はデータベースアプリケーションの信頼性を確保するために非常に重要です。適切な粒度で@Transactionalを適用し、読み取り専用トランザクションやロールバック条件などの属性を適切に設定することが、アプリケーションのパフォーマンスと堅牢性につながります。
ページングとソート
Webアプリケーションやエンタープライズアプリケーションでは、大量のデータを一度に表示するのではなく、ページ単位で分割して表示したり(ページング)、特定の基準で並べ替えて表示したり(ソート)することがよくあります。Spring Dataは、これらの機能をRepositoryレベルで簡単に実現する方法を提供しています。
PagingAndSortingRepository またはそれを継承する JpaRepository を利用することで、ページングとソートに関連するメソッドが利用可能になります。これらのメソッドは、org.springframework.data.domain.Pageable インターフェースと org.springframework.data.domain.Sort クラスを引数として受け取ります。
Pageable と Sort
Sort: 結果の並び替え方法を指定します。フィールド名と昇順/降順(ASC/DESC)を指定します。複数のフィールドでソートすることも可能です。Pageable: 取得したいページの情報を指定します。ページ番号、1ページあたりのアイテム数、および(オプションで)ソート情報を指定します。
PageableとSortのインスタンスは、通常PageRequestやSort.by()のようなファクトリメソッドを使用して作成します。
“`java
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
// priceで昇順にソート
Sort sortByPriceAsc = Sort.by(“price”).ascending();
// priceで昇順、stockで降順にソート
Sort sortByPriceAscAndStockDesc = Sort.by(“price”).ascending().and(Sort.by(“stock”).descending());
// ページング情報の作成(1ページ目、サイズ10件、価格昇順でソート)
Pageable pageable = PageRequest.of(0, 10, Sort.by(“price”).ascending());
// ソート情報を含まないページング情報(Repositoryメソッドでソート指定する場合など)
// Pageable pageableWithoutSort = PageRequest.of(0, 10);
“`
Repositoryメソッドでの利用
JpaRepositoryは、findAll(Pageable pageable) や findAll(Sort sort) といったメソッドを提供しています。また、独自のクエリメソッドでも、最後のパラメータとして Pageable や Sort を受け取るように定義することで、ページングやソートを適用できます。
RepositoryメソッドがPageableを引数に取る場合、戻り値は通常 org.springframework.data.domain.Page<T> となります。Pageオブジェクトは、現在のページのデータリストに加え、総アイテム数、総ページ数、現在のページ番号、1ページあたりのサイズなど、ページングに関連する様々な情報を含んでいます。
“`java
// src/main/java/com/example/demo/repository/ProductRepository.java (さらに修正版)
package com.example.demo.repository;
import com.example.demo.entity.Product;
import org.springframework.data.domain.Page; // Pageクラスのimport
import org.springframework.data.domain.Pageable; // Pageableインターフェースのimport
import org.springframework.data.domain.Sort; // Sortクラスのimport
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
// … (既存のimportとインターフェース定義) …
public interface ProductRepository extends JpaRepository
// ... (既存のクエリメソッドや@Queryメソッド) ...
// 全件をページングして取得
Page<Product> findAll(Pageable pageable); // JpaRepositoryが標準で提供
// 全件をソートして取得
List<Product> findAll(Sort sort); // JpaRepositoryが標準で提供
// 特定の条件でフィルタリングし、ページングして取得
// クエリメソッド: priceが指定値より大きい商品をページング
Page<Product> findByPriceGreaterThan(double minPrice, Pageable pageable);
// クエリメソッド: priceが指定値より大きい商品をソート
List<Product> findByPriceGreaterThan(double minPrice, Sort sort);
// @Queryメソッド: 名前の一部を含む商品を検索し、ページング
@Query("SELECT p FROM Product p WHERE p.name LIKE %:name%")
Page<Product> searchByNameLike(@Param("name") String name, Pageable pageable);
}
“`
利用例
Service層やController層でこれらのメソッドを利用します。
“`java
// ProductService.java (メソッド追加)
// … (既存コード) …
@Service
@Transactional(readOnly = true)
public class ProductService {
// ... (既存の注入とメソッド) ...
// 全件をページングして取得
public Page<Product> getProductsByPage(int page, int size, String sortBy, String sortDirection) {
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
return productRepository.findAll(pageable);
}
// priceが指定値より大きい商品をページングして取得
public Page<Product> getHighPriceProductsByPage(double minPrice, int page, int size, String sortBy, String sortDirection) {
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
return productRepository.findByPriceGreaterThan(minPrice, pageable);
}
// 名前の一部で検索し、ソートして取得
public List<Product> searchProductsAndSort(String namePart, String sortBy, String sortDirection) {
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
// searchByNameLike(@Param("name") String name) メソッドに Sort を追加することも可能
// List<Product> searchByNameLike(@Param("name") String name, Sort sort);
// そのメソッドがあれば return productRepository.searchByNameLike(namePart, sort);
// ここではクエリメソッド findByNameContaining と findAll(Sort) を組み合わせる例として
List<Product> products = productRepository.findByNameContaining(namePart);
// ソートはメモリ上で行われるか、別の Repository メソッドを使う必要がある
// より効率的には、最初からソートを Repository クエリに含めるべき
// 例: List<Product> findByNameContaining(String part, Sort sort);
// List<Product> products = productRepository.findByNameContaining(namePart, sort);
return products; // ソート済みの結果を返す(RepositoryメソッドにSort引数を追加した場合)
}
// ...
}
“`
Page オブジェクトの活用:
Page<T> オブジェクトからは、以下の情報を取得できます。
getContent(): 現在のページのデータリストを取得。getTotalElements(): クエリ全体で一致したアイテムの総数を取得。getTotalPages(): 総ページ数を取得。getNumber(): 現在のページ番号を取得(0から始まる)。getSize(): 1ページあたりのアイテム数を取得。getNumberOfElements(): 現在のページのアイテム数を取得。isFirst(),isLast(),hasNext(),hasPrevious(): 最初/最後のページか、次/前のページが存在するかを確認。
これらの情報は、WebアプリケーションのUIでページングUI(「前へ」「次へ」、ページ番号リスト、総件数表示など)を構築する際に非常に役立ちます。
Specification (Criteria APIベース)
クエリメソッドや@Queryアノテーションは静的なクエリに適していますが、ユーザー入力に基づいて動的に検索条件が組み立てられるようなケースには向いていません。例えば、Webフォームでユーザーが任意に検索フィールドを組み合わせて絞り込み検索を行うような場合です。
このような動的なクエリ構築には、JPAのCriteria APIを利用するのが一般的です。Spring Data JPAは、このCriteria APIをより簡単に利用するための機能としてSpecificationを提供しています。
Specificationは、検索条件をJavaオブジェクトとして表現し、それを組み合わせて複雑なWHERE句を構築できるようにします。これを利用するには、Repositoryインターフェースが org.springframework.data.jpa.domain.Specification を実行するための JpaSpecificationExecutor<T> インターフェースを継承する必要があります。
“`java
// src/main/java/com/example/demo/repository/ProductRepository.java (さらに修正版)
package com.example.demo.repository;
import com.example.demo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; // import
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification; // Specificationインターフェースのimport
// JpaRepository と JpaSpecificationExecutor を継承
public interface ProductRepository extends JpaRepository
// findOne(Specification
// findAll(Specification
// findAll(Specification
// findAll(Specification
// count(Specification
// など、Specificationを引数にとるメソッドが利用可能になる
}
“`
Specificationの作成
Specification<T> インターフェースは単一の抽象メソッド toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) を持ちます。このメソッド内でCriteria APIを使用して javax.persistence.criteria.Predicate オブジェクト(WHERE句の一部に相当)を構築します。
Root<T> root: クエリの対象となるエンティティのルート。プロパティへのアクセスに使用します。CriteriaQuery<?> query: Criteriaクエリ全体。GROUP BY, HAVING, ORDER BY などを設定する際に使用します。CriteriaBuilder criteriaBuilder: Predicateや式(Expression)を生成するためのファクトリ。
Specificationの例:
“`java
// src/main/java/com/example/demo/spec/ProductSpecifications.java
package com.example.demo.spec;
import com.example.demo.entity.Product;
import org.springframework.data.jpa.domain.Specification;
// staticインポートすると Predicate や CriteriaBuilder を省略できる
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
// jakarta.persistence.criteria.* if using Spring Boot 3+
public class ProductSpecifications {
// 名前の一部を含む商品を検索するSpecification
public static Specification<Product> nameContains(String namePart) {
return (root, query, cb) -> {
if (namePart == null || namePart.isEmpty()) {
return null; // 条件がない場合は null を返す(全てのレコードが対象になる)
}
// cb.like( root.get("name"), "%" + namePart + "%" ) は LIKE '%namePart%' と同等
return cb.like(cb.lower(root.get("name")), "%" + namePart.toLowerCase() + "%"); // 大文字小文字を区別しない場合
};
}
// 価格が指定値以上の商品を検索するSpecification
public static Specification<Product> priceGreaterThanOrEqualTo(double minPrice) {
return (root, query, cb) -> {
// cb.greaterThanOrEqualTo( root.get("price"), minPrice ) は price >= minPrice と同等
return cb.greaterThanOrEqualTo(root.get("price"), minPrice);
};
}
// 在庫が指定値以下の商品を検索するSpecification
public static Specification<Product> stockLessThanOrEqualTo(int maxStock) {
return (root, query, cb) -> {
// cb.lessThanOrEqualTo( root.get("stock"), maxStock ) は stock <= maxStock と同等
return cb.lessThanOrEqualTo(root.get("stock"), maxStock);
};
}
}
“`
Specificationは、上記の例のように静的メソッドとして作成することが多いですが、Specificationインターフェースを実装するクラスを作成することも可能です。
Specificationの組み合わせ
Specificationの利点は、and() や or() メソッドを使って複数の条件を簡単に組み合わせられることです。
“`java
import static com.example.demo.spec.ProductSpecifications.*; // 上記の static メソッドをインポート
// 名前が “lap” を含み、かつ価格が1000以上の商品を検索
Specification
// 名前が “mouse” を含むか、または在庫が10以下の商品を検索
Specification
“`
Specification.where(Specification<T> spec) は、最初のSpecificationを作成するための便利なメソッドです。null を指定すると、条件なし(全件対象)のSpecificationが作成されます。
利用例
ProductServiceでこれらのSpecificationを利用してみましょう。
“`java
// ProductService.java (メソッド追加)
package com.example.demo.service;
import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.data.jpa.domain.Specification; // Specification import
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import java.util.List;
// static import ProductSpecifications
import static com.example.demo.spec.ProductSpecifications.*;
@Service
@Transactional(readOnly = true)
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// Specificationを使って商品を検索 (全件)
public List<Product> findProductsBySpecification(Specification<Product> spec) {
return productRepository.findAll(spec);
}
// Specificationを使って商品を検索 (ソート付き)
public List<Product> findProductsBySpecification(Specification<Product> spec, Sort sort) {
return productRepository.findAll(spec, sort);
}
// Specificationを使って商品を検索 (ページング付き)
public Page<Product> findProductsBySpecification(Specification<Product> spec, Pageable pageable) {
return productRepository.findAll(spec, pageable);
}
// 例: ユーザー入力に基づいて動的に検索
public List<Product> dynamicSearch(String namePart, Double minPrice, Integer maxStock, String sortBy, String sortDirection) {
Specification<Product> spec = Specification.where(null); // 初期条件なし (全件)
// 条件があれば追加
if (namePart != null && !namePart.isEmpty()) {
spec = spec.and(nameContains(namePart));
}
if (minPrice != null) {
spec = spec.and(priceGreaterThanOrEqualTo(minPrice));
}
if (maxStock != null) {
spec = spec.and(stockLessThanOrEqualTo(maxStock));
}
// ソート情報の作成
Sort sort = Sort.unsorted(); // デフォルトはソートなし
if (sortBy != null && !sortBy.isEmpty()) {
Sort.Direction direction = Sort.Direction.ASC;
if (sortDirection != null && sortDirection.equalsIgnoreCase("desc")) {
direction = Sort.Direction.DESC;
}
sort = Sort.by(direction, sortBy);
}
// Specification と Sort を使って検索
return productRepository.findAll(spec, sort);
}
// 例: Specification を使ってカウント
public long countProductsBySpecification(Specification<Product> spec) {
return productRepository.count(spec);
}
// ... (既存メソッド) ...
}
“`
Specificationは、Criteria APIの知識が必要になりますが、動的なクエリ構築や複雑な条件の組み合わせをエレガントに記述できる強力な機能です。
Querydsl連携
Specificationと同様に、動的なクエリを構築するためのもう一つの強力な選択肢としてQuerydslがあります。Querydslは、静的型付けされたFluent APIを使用してクエリを記述できるライブラリです。SQL, JPQL, MongoDBなど様々なバックエンドをサポートしています。
Spring Data JPAはQuerydslとの連携をサポートしており、Repositoryインターフェースが QuerydslPredicateExecutor<T> を継承することで利用できます。
“`java
// src/main/java/com/example/demo/repository/ProductRepository.java (さらに修正版)
package com.example.demo.repository;
import com.example.demo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor; // import
// JpaRepository と QuerydslPredicateExecutor を継承
public interface ProductRepository extends JpaRepository
// findOne(Predicate predicate)
// findAll(Predicate predicate)
// findAll(Predicate predicate, Sort sort)
// findAll(Predicate predicate, Pageable pageable)
// count(Predicate predicate)
// exists(Predicate predicate)
// など、Querydsl Predicateを引数にとるメソッドが利用可能になる
}
“`
Querydslの設定とQクラスの生成
Querydslを使用するためには、MavenやGradleのビルド設定にQuerydsl APT (Annotation Processor Tool) を追加し、ビルド時にクエリタイプ(Qクラス)を生成する必要があります。
Mavenの場合(pom.xmlの一部):
“`xml
“`
Gradleの場合(build.gradleの一部):
“`gradle
plugins {
// …
id ‘java’
id ‘org.springframework.boot’ version ‘2.7.x’ // または 3.x.x
id ‘io.spring.dependency-management’ version ‘1.0.x’ // または 1.x.x
id “com.ewerk.gradle.plugins.querydsl” version “1.0.10” // Querydsl プラグイン
}
dependencies {
// …
implementation ‘org.springframework.boot:spring-boot-starter-data-jpa’
implementation ‘com.querydsl:querydsl-jpa:4.4.0’ // または 5.x.x
// annotationProcessor ‘com.querydsl:querydsl-apt:4.4.0:jpa’ // または 5.x.x
// spring-boot-starter-data-jpa が hibernate-core を含む場合、jakarta classifier は不要な場合がある
// または
// implementation ‘com.querydsl:querydsl-jpa:5.0.0:jakarta’ // Spring Boot 3+ の場合
// annotationProcessor ‘com.querydsl:querydsl-apt:5.0.0:jakarta’ // Spring Boot 3+ の場合
// Querydsl build dependencies
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main {
java {
srcDirs = ['src/main/java', querydslDir]
}
}
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
}
“`
ビルドを実行すると、com.example.demo.entity パッケージ内のエンティティに対して、QProduct.java のようなクエリタイプクラスが生成されます。これらのQクラスを使用して、型安全なクエリを構築します。
Predicateの作成
Querydslでは、検索条件を Predicate オブジェクトとして表現します。生成されたQクラスのインスタンスを使用してPredicateを構築します。
“`java
// src/main/java/com/example/demo/service/ProductService.java (Querydsl 利用例)
package com.example.demo.service;
import com.example.demo.entity.Product;
import com.example.demo.entity.QProduct; // Qクラスのimport
import com.example.demo.repository.ProductRepository;
import com.querydsl.core.BooleanBuilder; // 条件を組み合わせるためのクラス
import com.querydsl.core.types.Predicate; // Predicateインターフェースのimport
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import java.util.List;
@Service
@Transactional(readOnly = true)
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// Querydsl Predicate を使って検索 (全件)
public List<Product> findProductsByPredicate(Predicate predicate) {
// Predicate を null にすると全件検索
return (List<Product>) productRepository.findAll(predicate);
}
// Querydsl Predicate を使って検索 (ソート付き)
public List<Product> findProductsByPredicate(Predicate predicate, Sort sort) {
return (List<Product>) productRepository.findAll(predicate, sort);
}
// Querydsl Predicate を使って検索 (ページング付き)
public Page<Product> findProductsByPredicate(Predicate predicate, Pageable pageable) {
return productRepository.findAll(predicate, pageable);
}
// 例: ユーザー入力に基づいて動的にQuerydslで検索
public List<Product> dynamicSearchWithQuerydsl(String namePart, Double minPrice, Integer maxStock, String sortBy, String sortDirection) {
QProduct product = QProduct.product; // 生成されたQクラスのインスタンス
BooleanBuilder builder = new BooleanBuilder(); // 条件を組み立てるビルダー
// 条件を追加
if (namePart != null && !namePart.isEmpty()) {
// Qクラスのフィールドは型安全なクエリオブジェクト
builder.and(product.name.containsIgnoreCase(namePart)); // containsIgnoreCase は大文字小文字を区別しない LIKE
}
if (minPrice != null) {
builder.and(product.price.goe(minPrice)); // goe() は >=
}
if (maxStock != null) {
builder.and(product.stock.loe(maxStock)); // loe() は <=
}
Predicate finalPredicate = builder.getValue(); // 組み立てられた Predicate を取得
// ソート情報の作成 (Spring Data の Sort をそのまま使用)
Sort sort = Sort.unsorted();
if (sortBy != null && !sortBy.isEmpty()) {
Sort.Direction direction = Sort.Direction.ASC;
if (sortDirection != null && sortDirection.equalsIgnoreCase("desc")) {
direction = Sort.Direction.DESC;
}
sort = Sort.by(direction, sortBy);
}
// Predicate と Sort を使って検索
return (List<Product>) productRepository.findAll(finalPredicate, sort);
}
// ... (既存メソッド) ...
}
“`
QuerydslはSpecificationよりもさらに型安全性が高く、リファクタリングにも強いというメリットがあります。Criteria APIの記述が冗長に感じられる場合や、複数のデータストアで共通のクエリ構築ロジックを使用したい場合に強力な選択肢となります。ただし、Qクラス生成のためのビルド設定が必要になります。
カスタムRepository実装
Spring Dataの提供する標準メソッド、クエリメソッド、@Query, Specification, Querydsl だけでは、要件を満たせない場合があります。例えば、複数のエンティティに対する複雑な集計処理や、既存のストアドプロシージャの呼び出しなどです。
このような場合、Repositoryインターフェースの一部または全体に対して、独自の実装クラスを提供することができます。
Spring Dataは、Repositoryインターフェースに定義されたメソッドのうち、標準やクエリメソッドの命名規則に合わないものを、カスタム実装として扱う仕組みを提供しています。
カスタム実装の手順
-
カスタムメソッドを定義するインターフェースを作成: 既存のRepositoryインターフェースとは別に、カスタムメソッドだけを定義するインターフェースを作成します。インターフェース名には特にルールはありませんが、慣習としてRepositoryインターフェース名に
Customなどを付け加えることが多いです。
“`java
// src/main/java/com/example/demo/repository/ProductRepositoryCustom.java
package com.example.demo.repository;import com.example.demo.entity.Product;
import java.util.List;public interface ProductRepositoryCustom {
// 在庫が指定数を下回っている商品を、一括で補充するカスタムメソッド
int replenishLowStockProducts(int minStock, int replenishAmount);
}
2. **カスタムメソッドを含むようにメインのRepositoryインターフェースを変更**: 作成したカスタムインターフェースを、メインのRepositoryインターフェースに継承させます。java
// src/main/java/com/example/demo/repository/ProductRepository.java (さらに修正版)
package com.example.demo.repository;import com.example.demo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;// Customインターフェースを継承する
public interface ProductRepository extends JpaRepository,
JpaSpecificationExecutor,
QuerydslPredicateExecutor,
ProductRepositoryCustom // 追加
{
// … (既存のメソッド) …
}
3. **カスタム実装クラスを作成**: 作成したカスタムインターフェースを実装するクラスを作成します。クラス名には厳密なルールがあり、「メインのRepositoryインターフェース名」+「Impl」とします(デフォルト設定の場合)。例えば、`ProductRepository`に対する実装クラスは`ProductRepositoryImpl`となります。このクラスはSpring Beanとして扱われるため、他のRepositoryなどを注入して使用できます。java
// src/main/java/com/example/demo/repository/impl/ProductRepositoryImpl.java
package com.example.demo.repository.impl;import com.example.demo.entity.Product;
import com.example.demo.repository.ProductRepositoryCustom;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository; // @Repositoryは不要な場合が多いが、つけても問題ないimport javax.persistence.EntityManager; // JPAのEntityManagerを注入
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional; // JPAでは javax.transaction.Transactional を使用することもあるが、Springの@Transactionalが一般的// クラス名は「Repositoryインターフェース名 + Impl」がデフォルトの命名規則
public class ProductRepositoryImpl implements ProductRepositoryCustom {@PersistenceContext // EntityManager を注入 private EntityManager entityManager; // またはコンストラクタインジェクションでも可 // private final EntityManager entityManager; // @Autowired // public ProductRepositoryImpl(EntityManager entityManager) { // this.entityManager = entityManager; // } // JpaRepository のメソッドをここで再実装してはいけない // ここでは ProductRepositoryCustom で定義したメソッドのみを実装する @Override @Transactional // カスタムメソッドもトランザクション内で実行する必要がある public int replenishLowStockProducts(int minStock, int replenishAmount) { // EntityManager を使って直接JPQLやNative SQLを実行 String jpql = "UPDATE Product p SET p.stock = p.stock + :amount WHERE p.stock < :minStock"; int updatedCount = entityManager.createQuery(jpql) .setParameter("amount", replenishAmount) .setParameter("minStock", minStock) .executeUpdate(); // 更新系クエリの実行 System.out.println("Replenished stock for " + updatedCount + " products."); return updatedCount; }}
``[Repositoryインターフェース名]Impl
デフォルトでは、Spring Dataはという名前のクラスをスキャンしてカスタム実装として紐付けます。この命名規則は、@EnableJpaRepositoriesアノテーションのrepositoryImplementationPostfix` 属性で変更可能です。
これで、ProductRepository を注入して使用する際に、Spring Dataが自動生成したメソッドに加えて、replenishLowStockProducts メソッドも呼び出せるようになります。Spring Dataは、メソッド名を見て、標準/クエリメソッドで対応できるものは自動生成実装で、対応できないものはカスタム実装クラスで提供されているメソッドを呼び出すように内部的に処理します。
カスタム実装は、Spring Dataの自動生成機能では対応できない、より複雑または特殊なデータアクセスロジックを実装するための手段です。
Spring Data REST (補足)
Spring Data RESTは、Spring Data Repositoryから自動的にRESTful Webサービスを生成するプロジェクトです。Repositoryを定義するだけで、そのエンティティに対するCRUD操作を行うRESTエンドポイント(例: /products, /products/{id})が自動的に公開されます。
これはRepositoryの直接的な機能ではありませんが、データアクセス層を開発する上で非常に便利なツールであり、Repositoryと密接に関連しています。簡単な管理画面やAPIを素早く構築したい場合に非常に有用ですが、細かいカスタマイズや複雑なビジネスロジックが必要な場合は、別途Controller層を作成する必要があります。
Spring Data RESTを利用するには、spring-boot-starter-data-rest 依存関係を追加します。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
依存関係を追加し、Repositoryを定義するだけで、例えば /products エンドポイントからJSON形式で商品リストが取得できるようになります。
その他のSpring Dataプロジェクト
Spring Dataは、リレーショナルデータベース向けのSpring Data JPAだけでなく、様々なデータストアに対応したプロジェクトを提供しています。
- Spring Data MongoDB
- Spring Data Redis
- Spring Data Elasticsearch
- Spring Data Neo4j (グラフデータベース)
- Spring Data Cassandra
- Spring Data Couchbase
- Spring Data R2DBC (リアクティブデータベースアクセス)
- …など多数
これらのプロジェクトでも、この記事で解説したRepositoryパターン(インターフェース定義、クエリメソッド、@Queryに相当するアノテーションなど)が共通のプログラミングモデルとして提供されています。データストアが変わっても、似たような開発スタイルでデータアクセス層を実装できるのがSpring Dataの大きなメリットです。
Repository利用時のベストプラクティス
Spring Data Repositoryを効果的に活用するためのいくつかのベストプラクティスを紹介します。
- Repositoryはデータアクセス専用とする: Repositoryインターフェース(およびカスタム実装クラス)は、データストアとのやり取りにのみ責任を持つべきです。ビジネスルールの適用や複数のRepositoryからのデータ集約といったビジネスロジックは、Service層に配置します。これにより、Repositoryは再利用可能になり、単体テストも容易になります。
- トランザクション境界を Service 層に置く: 複雑な操作や複数のRepositoryメソッド呼び出しを含むビジネスロジックは、Service層のメソッドとして定義し、そこに
@Transactionalを付与するのが一般的です。これにより、ビジネスロジックの単位でアトミック性を確保できます。Repositoryメソッド個々にはSpring Dataがデフォルトでトランザクションを適用しますが、Service層で明示的に管理することで、より柔軟なトランザクション制御が可能になります。 - 読み取り専用トランザクションを活用する: データ変更を伴わない検索メソッドを含むServiceメソッドや、Repositoryメソッド自体に
@Transactional(readOnly = true)を付与します。これによりパフォーマンスが向上する可能性があります。 - 遅延ロード (Lazy Loading) と早期ロード (Eager Loading): JPAの関連付け(@OneToMany, @ManyToOneなど)には、データの取得タイミングとして遅延ロード(デフォルト)と早期ロードがあります。遅延ロードは必要になるまで関連データを取得しないためパフォーマンスに優れますが、後から関連データにアクセスする際に別途クエリが発生する(N+1問題)可能性があります。早期ロードは関連データをまとめて取得するためN+1問題を回避できますが、不要なデータまで取得してしまいパフォーマンスが低下する可能性があります。関連データのロード戦略は、ユースケースに応じて適切に選択し、必要に応じてフェッチタイプをオーバライド(JPQLのFETCH JOINやEntityGraphなど)することが重要です。
- N+1問題とその対策: 複数の関連エンティティを遅延ロードで取得する際に発生するN+1問題(親エンティティN件に対して、子エンティティを取得するためにN+1回のクエリが発生する)は、パフォーマンスのボトルネックになりやすいです。
- FETCH JOIN: JPQLの
JOIN FETCH句を使用すると、親エンティティを取得する際に子エンティティもまとめて(一回のクエリで)取得できます。@QueryアノテーションでJPQLを記述する際に利用できます。 - EntityGraph:
@EntityGraphアノテーションをRepositoryメソッドに付与することで、特定の関連を早期ロードするように指定できます。クエリメソッドや@Queryメソッドと組み合わせて使用可能です。 - Batch Size: JPAプロバイダーの設定で、関連エンティティをまとめて取得する際のバッチサイズを指定できます。これにより、N+1問題を完全に解消するわけではありませんが、クエリ回数を減らすことができます(N+1回からN/BatchSize + 1 回程度)。
- FETCH JOIN: JPQLの
- Repositoryテスト: Repositoryの機能が期待通りに動作するかを確認するためにテストを書くことが重要です。
- 統合テスト: Spring Data JPAを使用する場合、インメモリデータベース(H2など)やテスト用の外部データベースを使用して、Repositoryメソッドが正しくSQLを生成し、データベースと連携できるかを確認する統合テストが効果的です。
@DataJpaTestアノテーションを使用すると、JPA関連のSpringコンテキスト(DataSource, EntityManager, Spring Data Repositoriesなど)のみをロードして高速にテストを実行できます。 - ユニットテスト: Repositoryインターフェース自体はテスト対象にはなり得ません(インターフェースなのでロジックがない)。ただし、カスタム実装クラスを作成した場合は、そのクラス内のロジックに対してモックなどを使用したユニットテストを行うことが可能です。
- 統合テスト: Spring Data JPAを使用する場合、インメモリデータベース(H2など)やテスト用の外部データベースを使用して、Repositoryメソッドが正しくSQLを生成し、データベースと連携できるかを確認する統合テストが効果的です。
- Repositoryのメソッド名規約を理解する: クエリメソッドを使用する際は、Spring Dataの命名規則を正確に理解しておく必要があります。複雑なクエリになる場合は、無理にクエリメソッドを使わず、
@QueryやSpecification、Querydslを検討しましょう。 - Optionalを適切に使う:
findByIdやfindFirstBy...のように単一のエンティティを返す可能性があるメソッドは、戻り値の型をOptional<T>にすることが推奨されます。これにより、結果が存在しない場合(null)のハンドリングが明示的になり、NullPointerExceptionを防ぐことができます。
これらのベストプラクティスを実践することで、Spring Data Repositoryを使用したデータアクセス層の実装をより効率的かつ堅牢に行うことができます。
まとめ
この記事では、Spring BootにおけるRepositoryの概念から始まり、Spring Data JPAを中心にその基本的な使い方と実践的な機能について詳細に解説しました。
- Repositoryはデータアクセス層を抽象化し、ビジネスロジックから永続化の詳細を隠蔽します。
- Spring Dataプロジェクトは、Repositoryインターフェースを定義するだけで、CRUD操作や様々なクエリ実行メソッドの実装を自動的に提供します。
- Spring Data JPAを使用することで、JPAエンティティに対するデータアクセスを効率的に開発できます。
- 基本的なCRUD操作は、
JpaRepositoryなどのインターフェースを継承するだけで利用できます。 - クエリメソッドは、特定の命名規則に従ってメソッドを定義することで、自動的に対応するクエリが生成される非常に便利な機能です。
@Queryアノテーションを使用すると、JPQLやNative SQLによるカスタムクエリを定義し実行できます。データ変更クエリには@Modifyingアノテーションと@Transactionalアノテーションが必要です。- トランザクション管理は、
@TransactionalアノテーションをService層のメソッドに付与することで、複数のデータベース操作をアトミックに実行できます。 - ページングとソートは、
PageableとSortインタフェースをRepositoryメソッドの引数に指定することで、簡単に実現できます。 - SpecificationやQuerydslは、動的なクエリ構築や複雑な条件の組み合わせが必要な場合に、型安全かつ柔軟な方法を提供します。
- Spring Dataの自動生成機能では対応できない複雑なデータアクセスロジックは、カスタムRepository実装によって実現できます。
Spring Dataは、データアクセス層の開発を大幅に簡素化し、開発者がビジネスロジックの実装により集中できるようにします。この記事で解説した基本的な機能と実践的なテクニックを理解することで、Spring Bootアプリケーションにおける効率的で堅牢なデータアクセス層を構築できるようになるはずです。
Repositoryはアプリケーションの根幹となる部分の一つです。Spring Dataの強力な機能を最大限に活用し、保守性の高いデータアクセス層を設計・実装していきましょう。