JPA/Hibernate パターン
Spring Boot でのデータモデリング、リポジトリ、パフォーマンスチューニングに使用します。
アクティベーションのタイミング
- JPA エンティティとテーブルマッピングの設計
- リレーションシップの定義(@OneToMany、@ManyToOne、@ManyToMany)
- クエリの最適化(N+1 防止、フェッチ戦略、プロジェクション)
- トランザクション、監査、ソフトデリートの設定
- ページネーション、並び替え、カスタムリポジトリメソッドの設定
- コネクションプーリング(HikariCP)またはセカンドレベルキャッシュのチューニング
エンティティ設計
@Entity
@Table(name = "markets", indexes = {
@Index(name = "idx_markets_slug", columnList = "slug", unique = true)
})
@EntityListeners(AuditingEntityListener.class)
public class MarketEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false, unique = true, length = 120)
private String slug;
@Enumerated(EnumType.STRING)
private MarketStatus status = MarketStatus.ACTIVE;
@CreatedDate private Instant createdAt;
@LastModifiedDate private Instant updatedAt;
}
監査を有効にする:
@Configuration
@EnableJpaAuditing
class JpaConfig {}
リレーションシップと N+1 防止
@OneToMany(mappedBy = "market", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PositionEntity> positions = new ArrayList<>();
- デフォルトは遅延ロードにし、必要な場合のみクエリで
JOIN FETCHを使用 - コレクションには
EAGERを避け、読み取りパスでは DTO プロジェクションを使用
@Query("select m from MarketEntity m left join fetch m.positions where m.id = :id")
Optional<MarketEntity> findWithPositions(@Param("id") Long id);
リポジトリパターン
public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
Optional<MarketEntity> findBySlug(String slug);
@Query("select m from MarketEntity m where m.status = :status")
Page<MarketEntity> findByStatus(@Param("status") MarketStatus status, Pageable pageable);
}
- 軽量クエリにはプロジェクションを使用:
public interface MarketSummary {
Long getId();
String getName();
MarketStatus getStatus();
}
Page<MarketSummary> findAllBy(Pageable pageable);
トランザクション
- サービスクラスのメソッドに
@Transactionalを付与 - 読み取りパスでは最適化のため
@Transactional(readOnly = true)を使用 - 伝搬設定を適切に選択し、長時間トランザクションを避ける
@Transactional
public Market updateStatus(Long id, MarketStatus status) {
MarketEntity entity = repo.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Market"));
entity.setStatus(status);
return Market.from(entity);
}
ページネーション
PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
Page<MarketEntity> markets = repo.findByStatus(MarketStatus.ACTIVE, page);
カーソルベースのページネーションの場合、JPQL に id > :lastId を追加し、並び替えを行います。
インデックスとパフォーマンス
- よく使うフィルター(
status、slug、外部キー)にインデックスを追加 - クエリパターンに合った複合インデックスを使用(
status, created_at) select *を避け、必要なカラムだけを投影saveAllとhibernate.jdbc.batch_sizeを使って書き込みをバッチ処理
コネクションプーリング(HikariCP)
推奨プロパティ:
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.validation-timeout=5000
PostgreSQL の LOB 処理には次を追加:
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
キャッシュ
- 1次キャッシュは EntityManager ごと。トランザクションをまたいでエンティティを保持しない
- 読み取りが多いエンティティには、慎重にセカンドレベルキャッシュを検討し、排除戦略を検証
マイグレーション
- Flyway または Liquibase を使用。本番環境では Hibernate の自動 DDL に頼らない
- マイグレーションを冪等かつ追加的に保ち、計画なしにカラムを削除しない
データアクセスのテスト
- 本番環境を反映するため、
@DataJpaTestと Testcontainers を推奨 - ログで SQL 効率を確認:
logging.level.org.hibernate.SQL=DEBUGおよびlogging.level.org.hibernate.orm.jdbc.bind=TRACEを設定してパラメータ値を表示
重要: エンティティは軽量に、クエリは意図的に、トランザクションは短く保つこと。フェッチ戦略とプロジェクションで N+1 を防止し、読み取り/書き込みパスに合わせてインデックスを作成する。