Skip to content

JpaRepositoryの中身

Spring Data JPAのJpaRepositoryは、データベース操作を簡潔に行うための強力なインターフェースです。この章では、JpaRepositoryの内部構造と動作メカニズムについて詳しく解説します。

JpaRepositoryは複数のインターフェースを継承しており、各インターフェースが異なる機能を提供しています。

public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
// JpaRepository固有のメソッド
List<T> findAll();
List<T> findAll(Sort sort);
List<T> findAllById(Iterable<ID> ids);
<S extends T> List<S> saveAll(Iterable<S> entities);
void flush();
<S extends T> S saveAndFlush(S entity);
void deleteInBatch(Iterable<T> entities);
void deleteAllInBatch();
T getOne(ID id);
<S extends T> List<S> findAll(Example<S> example);
<S extends T> List<S> findAll(Example<S> example, Sort sort);
}
  1. Repository<T, ID>: マーカーインターフェース。Spring Dataがリポジトリとして認識するための基準となります。

  2. CrudRepository<T, ID>: 基本的なCRUD操作を提供します。

    • save(S entity): エンティティを保存
    • findById(ID id): IDで検索
    • existsById(ID id): 存在確認
    • count(): 件数取得
    • deleteById(ID id): IDで削除
    • delete(T entity): エンティティで削除
    • deleteAll(): 全削除
  3. PagingAndSortingRepository<T, ID>: ページングとソート機能を提供します。

    • findAll(Sort sort): ソート指定で全件取得
    • findAll(Pageable pageable): ページング指定で取得
  4. QueryByExampleExecutor<T>: Exampleクエリを実行する機能を提供します。

Spring Data JPAの最も強力な機能の一つが、メソッド名から自動的にクエリを生成する機能です。

public interface UserRepository extends JpaRepository<User, Long> {
// 単純な検索
List<User> findByName(String name);
User findByEmail(String email);
// 複数条件
List<User> findByNameAndEmail(String name, String email);
List<User> findByNameOrEmail(String name, String email);
// 比較演算子
List<User> findByAgeGreaterThan(int age);
List<User> findByAgeLessThan(int age);
List<User> findByAgeBetween(int minAge, int maxAge);
// Nullチェック
List<User> findByEmailIsNotNull();
List<User> findByEmailIsNull();
// Like検索
List<User> findByNameContaining(String name);
List<User> findByNameLike(String name);
List<User> findByNameStartingWith(String prefix);
List<User> findByNameEndingWith(String suffix);
// ソート
List<User> findByNameOrderByAgeAsc(String name);
List<User> findByNameOrderByAgeDesc(String name);
// 件数制限
User findFirstByName(String name);
User findTopByName(String name);
List<User> findTop10ByName(String name);
}
public interface UserRepository extends JpaRepository<User, Long> {
// 関連エンティティのフィールドで検索
List<User> findByAddressCity(String city);
List<User> findByAddressZipCode(String zipCode);
// コレクション関連
List<User> findByOrdersStatus(OrderStatus status);
List<User> findByOrdersTotalAmountGreaterThan(BigDecimal amount);
}

@Queryアノテーションによるカスタムクエリ

Section titled “@Queryアノテーションによるカスタムクエリ”

メソッド名による自動生成では対応できない複雑なクエリは、@Queryアノテーションを使用して明示的に定義できます。

JPQL(Java Persistence Query Language)を使用

Section titled “JPQL(Java Persistence Query Language)を使用”
public interface UserRepository extends JpaRepository<User, Long> {
// JPQLクエリ
@Query("SELECT u FROM User u WHERE u.email = ?1")
User findByEmailAddress(String email);
// パラメータ名による指定(推奨)
@Query("SELECT u FROM User u WHERE u.name = :name AND u.age > :age")
List<User> findByNameAndAgeGreaterThan(@Param("name") String name, @Param("age") int age);
// ネイティブクエリ
@Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)
User findByEmailNative(String email);
// 更新クエリ
@Modifying
@Query("UPDATE User u SET u.name = :name WHERE u.id = :id")
int updateUserName(@Param("id") Long id, @Param("name") String name);
// 削除クエリ
@Modifying
@Query("DELETE FROM User u WHERE u.email = :email")
int deleteByEmail(@Param("email") String email);
}
public interface UserRepository extends JpaRepository<User, Long> {
// ネイティブクエリではエンティティのフィールド名ではなく、テーブルのカラム名を使用
@Query(value = "SELECT u.id, u.name, u.email FROM users u WHERE u.age > :age",
nativeQuery = true)
List<Object[]> findUsersByAgeNative(@Param("age") int age);
// 結果をDTOにマッピング
@Query(value = "SELECT new com.example.dto.UserDTO(u.id, u.name, u.email) " +
"FROM User u WHERE u.age > :age")
List<UserDTO> findUsersByAgeAsDTO(@Param("age") int age);
}

JpaRepositoryPagingAndSortingRepositoryを継承しているため、ページングとソート機能を簡単に使用できます。

public interface UserRepository extends JpaRepository<User, Long> {
// ページングなしのソート
List<User> findByName(String name, Sort sort);
// ページング付き検索
Page<User> findByName(String name, Pageable pageable);
// カスタムクエリでのページング
@Query("SELECT u FROM User u WHERE u.age > :age")
Page<User> findUsersByAge(@Param("age") int age, Pageable pageable);
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public Page<User> getUsers(int page, int size, String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy).ascending());
return userRepository.findAll(pageable);
}
public Page<User> searchUsers(String name, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return userRepository.findByName(name, pageable);
}
}

JpaRepositoryはバッチ処理のためのメソッドを提供しています。

public interface UserRepository extends JpaRepository<User, Long> {
// バッチ保存
<S extends T> List<S> saveAll(Iterable<S> entities);
// バッチ削除
void deleteInBatch(Iterable<T> entities);
void deleteAllInBatch();
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void saveUsersInBatch(List<User> users) {
// 通常のsaveAllは1件ずつINSERT文を実行
userRepository.saveAll(users);
// バッチサイズを設定して最適化
// application.propertiesに以下を追加:
// spring.jpa.properties.hibernate.jdbc.batch_size=50
// spring.jpa.properties.hibernate.order_inserts=true
// spring.jpa.properties.hibernate.order_updates=true
}
@Transactional
public void deleteUsersInBatch(List<User> users) {
// バッチ削除(IN句を使用)
userRepository.deleteInBatch(users);
}
}

複雑なロジックが必要な場合は、カスタムリポジトリを実装できます。

public interface UserRepositoryCustom {
List<User> findUsersWithComplexCriteria(String name, int minAge, String city);
void bulkUpdateUserStatus(List<Long> userIds, UserStatus status);
}
@Repository
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<User> findUsersWithComplexCriteria(String name, int minAge, String city) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
List<Predicate> predicates = new ArrayList<>();
if (name != null) {
predicates.add(cb.like(root.get("name"), "%" + name + "%"));
}
if (minAge > 0) {
predicates.add(cb.greaterThanOrEqualTo(root.get("age"), minAge));
}
if (city != null) {
predicates.add(cb.equal(root.get("address").get("city"), city));
}
query.where(predicates.toArray(new Predicate[0]));
return entityManager.createQuery(query).getResultList();
}
@Override
@Transactional
public void bulkUpdateUserStatus(List<Long> userIds, UserStatus status) {
String jpql = "UPDATE User u SET u.status = :status WHERE u.id IN :ids";
entityManager.createQuery(jpql)
.setParameter("status", status)
.setParameter("ids", userIds)
.executeUpdate();
}
}

リポジトリインターフェースの拡張

Section titled “リポジトリインターフェースの拡張”
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
// JpaRepositoryのメソッドとカスタムメソッドの両方が使用可能
}

必要なフィールドだけを取得することで、パフォーマンスを向上させることができます。

インターフェースベースのプロジェクション

Section titled “インターフェースベースのプロジェクション”
public interface UserSummary {
String getName();
String getEmail();
int getAge();
}
public interface UserRepository extends JpaRepository<User, Long> {
List<UserSummary> findByName(String name);
@Query("SELECT u.name as name, u.email as email FROM User u WHERE u.age > :age")
List<UserSummary> findUsersByAge(@Param("age") int age);
}
public class UserDTO {
private String name;
private String email;
public UserDTO(String name, String email) {
this.name = name;
this.email = email;
}
// getter/setter
}
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT new com.example.dto.UserDTO(u.name, u.email) FROM User u")
List<UserDTO> findAllAsDTO();
}

パフォーマンス最適化のポイント

Section titled “パフォーマンス最適化のポイント”
  1. N+1問題の回避: @EntityGraphJOIN FETCHを使用して関連エンティティを一度に取得します。
public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(attributePaths = {"orders", "address"})
List<User> findAll();
@Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id")
User findByIdWithOrders(@Param("id") Long id);
}
  1. 遅延ローディングの制御: @Transactionalを使用して、セッション内で関連データにアクセスできるようにします。

  2. バッチサイズの設定: application.propertiesでバッチサイズを設定して、バッチ処理を最適化します。

spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true

メソッド名クエリ vs @Query vs カスタムリポジトリ

Section titled “メソッド名クエリ vs @Query vs カスタムリポジトリ”

選択の判断基準:

// 1. メソッド名クエリ: シンプルなクエリに適している
public interface UserRepository extends JpaRepository<User, Long> {
// 利点: シンプル、型安全、コンパイル時に検証
// 欠点: 複雑なクエリには不向き
List<User> findByEmailAndStatus(String email, UserStatus status);
}
// 2. @Query: 複雑なクエリや最適化が必要な場合
public interface UserRepository extends JpaRepository<User, Long> {
// 利点: 柔軟性が高い、パフォーマンス最適化が可能
// 欠点: メンテナンスコストが高い
@Query("SELECT u FROM User u WHERE u.email = :email " +
"AND u.status = :status " +
"AND u.createdAt > :since")
List<User> findActiveUsersSince(@Param("email") String email,
@Param("status") UserStatus status,
@Param("since") LocalDateTime since);
}
// 3. カスタムリポジトリ: 非常に複雑なロジックや動的クエリ
public interface UserRepositoryCustom {
// 利点: 最大の柔軟性、複雑なロジックを実装可能
// 欠点: 実装コストが高い
List<User> findUsersWithComplexCriteria(UserSearchCriteria criteria);
}
// 判断基準:
// - シンプルな条件: メソッド名クエリ
// - 複雑な条件やJOIN: @Query
// - 動的クエリや複雑なロジック: カスタムリポジトリ

パフォーマンス最適化の深い理解

Section titled “パフォーマンス最適化の深い理解”

N+1問題の根本原因:

// 問題の本質を理解する
@Service
public class OrderService {
// N+1問題の発生メカニズム
public List<OrderDTO> getOrdersWithItems(Long customerId) {
// 1回のクエリ: 注文を取得
List<Order> orders = orderRepository.findByCustomerId(customerId);
// SQL: SELECT * FROM orders WHERE customer_id = ?
// 結果: 10件の注文
List<OrderDTO> dtos = new ArrayList<>();
for (Order order : orders) {
// N回のクエリ: 各注文のアイテムを取得
// 問題: 遅延ローディングにより、アクセス時にクエリが実行される
List<OrderItem> items = order.getItems();
// SQL: SELECT * FROM order_items WHERE order_id = ?
// 10件の注文に対して10回のクエリが実行される
dtos.add(convertToDTO(order, items));
}
// 合計: 1 + 10 = 11回のクエリ(N+1問題)
}
}
// 解決方法の理解
@Service
public class OrderService {
// 解決1: JOIN FETCH(最も効率的)
public List<OrderDTO> getOrdersWithItemsOptimized(Long customerId) {
// 1回のクエリで注文とアイテムを取得
List<Order> orders = orderRepository.findByCustomerIdWithItems(customerId);
// SQL: SELECT o.*, i.* FROM orders o
// LEFT JOIN order_items i ON o.id = i.order_id
// WHERE o.customer_id = ?
// 1回のクエリで完了
return orders.stream()
.map(order -> convertToDTO(order, order.getItems()))
.collect(Collectors.toList());
}
// 解決2: バッチフェッチ(複数の親エンティティがある場合)
public List<OrderDTO> getOrdersWithItemsBatch(Long customerId) {
List<Order> orders = orderRepository.findByCustomerId(customerId);
// 注文を取得後、バッチでアイテムを取得
// SQL: SELECT * FROM order_items
// WHERE order_id IN (?, ?, ?, ...)
// 2回のクエリで完了(1 + 1 = 2)
return orders.stream()
.map(order -> convertToDTO(order, order.getItems()))
.collect(Collectors.toList());
}
}

クエリ最適化の判断:

// 問題: すべてのデータを取得してからフィルタリング
@Query("SELECT u FROM User u")
List<User> findAllUsers();
// 解決: データベース側でフィルタリング
@Query("SELECT u FROM User u WHERE u.status = :status")
List<User> findUsersByStatus(@Param("status") UserStatus status);
// 問題: 不要なカラムを取得
@Query("SELECT u FROM User u")
List<User> findAllUsers();
// 解決: 必要なカラムのみを取得(プロジェクション)
@Query("SELECT new com.example.dto.UserSummaryDTO(u.id, u.name, u.email) " +
"FROM User u")
List<UserSummaryDTO> findUserSummaries();
// 問題: 全件取得してからページング
List<User> findAll(); // 10万件取得
// アプリケーション側でページング
// 解決: データベース側でページング
Page<User> findAll(Pageable pageable); // 必要な分だけ取得

トランザクション境界とRepository

Section titled “トランザクション境界とRepository”

Repository層でのトランザクション管理:

// 問題のあるコード: Repository層でトランザクションを管理
@Repository
@Transactional // 問題: Repository層でトランザクションを管理しない
public interface UserRepository extends JpaRepository<User, Long> {
}
// 解決: Service層でトランザクションを管理
@Repository // トランザクション管理なし
public interface UserRepository extends JpaRepository<User, Long> {
}
@Service
@Transactional // Service層でトランザクションを管理
public class UserService {
private final UserRepository userRepository;
@Transactional
public User createUserWithOrders(User user, List<Order> orders) {
User savedUser = userRepository.save(user);
for (Order order : orders) {
order.setUser(savedUser);
orderRepository.save(order);
}
return savedUser;
// すべての操作が1つのトランザクション内で実行される
}
}

JPA Repositoryの深い理解において重要なポイント:

  1. 適切な抽象化レベルの選択: メソッド名クエリ、@Query、カスタムリポジトリの使い分け
  2. パフォーマンス問題の根本理解: N+1問題の発生メカニズムと解決方法
  3. クエリ最適化: データベース側でのフィルタリングとページング
  4. トランザクション管理: Repository層ではなくService層で管理

シニアエンジニアとして考慮すべき点:

  • クエリの可読性: メソッド名から意図が明確か
  • パフォーマンス: 実際のクエリ実行計画を確認しているか
  • 保守性: 複雑なクエリは適切にドキュメント化されているか
  • テスト容易性: Repositoryのテストが容易か

JpaRepositoryは、これらの機能を組み合わせることで、効率的で保守性の高いデータアクセス層を構築できます。適切に使用することで、コードの量を大幅に削減し、パフォーマンスも向上させることができます。