金曜日, 8月 1, 2025
金曜日, 8月 1, 2025
- Advertisment -
ホームニューステックニュース Spring 第5回 ~エンタープライズアプリケーションの実装パターン~ (Spring Data JPAの基礎とリポジトリパターン) #SpringBoot

[速習] Spring 第5回 ~エンタープライズアプリケーションの実装パターン~ (Spring Data JPAの基礎とリポジトリパターン) #SpringBoot



[速習] Spring 第5回 ~エンタープライズアプリケーションの実装パターン~ (Spring Data JPAの基礎とリポジトリパターン) #SpringBoot

image.png

前回は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図で表すと以下のようになります。

エンティティ設計のポイント

  1. @Versionによる楽観的ロック9: 同時更新による不整合を防ぐ仕組み。例えば、2人が同時に在庫を更新しようとした時、後から更新した人にエラーを返して、データの不整合を防ぎます。

  2. 監査機能10の活用: @CreatedDate@LastModifiedDateで自動的に日時を記録。「いつ誰が作成・更新したか」という履歴を自動で残せます。

  3. 適切なフェッチ戦略11: FetchType.LAZYで遅延ロード。必要な時だけ関連データを取得することで、パフォーマンスを向上させます。

  4. 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を利用することで、データベース操作に関するコードを大幅に削減し、ビジネスロジックの実装により集中できるようになります。





Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -