EagerとLazy Loading
Eager LoadingとLazy Loading
Section titled “Eager LoadingとLazy Loading”JPAにおける**Eager Loading(積極的ローディング)とLazy Loading(遅延ローディング)**は、関連エンティティの取得戦略です。適切に使い分けることで、パフォーマンスを最適化できます。
フェッチ戦略の基本
Section titled “フェッチ戦略の基本”Eager Loading(積極的ローディング)
Section titled “Eager Loading(積極的ローディング)”Eager Loadingは、親エンティティを取得する際に、関連エンティティも同時に取得する戦略です。
@Entitypublic class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String name;
// EAGER: ユーザーを取得する際に、注文も同時に取得される @OneToMany(fetch = FetchType.EAGER, mappedBy = "user") private List<Order> orders = new ArrayList<>();}
@Entitypublic class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@ManyToOne(fetch = FetchType.EAGER) // EAGER: 注文を取得する際に、ユーザーも同時に取得 @JoinColumn(name = "user_id") private User user;}動作の流れ:
// Userを取得すると、関連するOrderも自動的に取得されるUser user = userRepository.findById(1L).orElseThrow();
// 以下のSQLが実行される:// 1. SELECT * FROM users WHERE id = 1// 2. SELECT * FROM orders WHERE user_id = 1 (自動的に実行される)
// トランザクション外でもアクセス可能List<Order> orders = user.getOrders(); // 既にロードされているため、追加のクエリは発生しないLazy Loading(遅延ローディング)
Section titled “Lazy Loading(遅延ローディング)”Lazy Loadingは、関連エンティティを実際にアクセスするまで取得しない戦略です。
@Entitypublic class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String name;
// LAZY: ユーザーを取得する際は、注文は取得しない @OneToMany(fetch = FetchType.LAZY, mappedBy = "user") private List<Order> orders = new ArrayList<>();}
@Entitypublic class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@ManyToOne(fetch = FetchType.LAZY) // LAZY: 注文を取得する際は、ユーザーは取得しない @JoinColumn(name = "user_id") private User user;}動作の流れ:
// Userを取得(Orderはまだ取得されない)User user = userRepository.findById(1L).orElseThrow();// SQL: SELECT * FROM users WHERE id = 1 (Orderのクエリは実行されない)
// Orderにアクセスした時点で取得されるList<Order> orders = user.getOrders();// SQL: SELECT * FROM orders WHERE user_id = 1 (この時点で実行される)N+1問題の詳細解説
Section titled “N+1問題の詳細解説”N+1問題とは?
Section titled “N+1問題とは?”N+1問題は、1回のクエリで親エンティティを取得し、その後N回のクエリで関連エンティティを取得する問題です。
問題のコード(Eager Loadingの場合)
Section titled “問題のコード(Eager Loadingの場合)”@Entitypublic class User { @OneToMany(fetch = FetchType.EAGER) private List<Order> orders;}
// サービスクラス@Servicepublic class UserService { public void displayAllUsers() { // 1回のクエリ: すべてのユーザーを取得 List<User> users = userRepository.findAll(); // SQL: SELECT * FROM users
// 各ユーザーごとにOrderを取得(N+1問題) for (User user : users) { // ユーザーごとにOrderを取得するクエリが実行される List<Order> orders = user.getOrders(); // SQL: SELECT * FROM orders WHERE user_id = ? // これがユーザー数分(N回)実行される }
// 合計: 1 + N回のクエリが実行される }}実行されるSQL:
-- 1回目: すべてのユーザーを取得SELECT * FROM users;
-- 2回目以降: 各ユーザーの注文を取得(ユーザー数分実行される)SELECT * FROM orders WHERE user_id = 1;SELECT * FROM orders WHERE user_id = 2;SELECT * FROM orders WHERE user_id = 3;-- ... ユーザー数分繰り返される問題のコード(Lazy Loadingの場合)
Section titled “問題のコード(Lazy Loadingの場合)”@Entitypublic class User { @OneToMany(fetch = FetchType.LAZY) private List<Order> orders;}
@Service@Transactionalpublic class UserService { public void displayAllUsers() { // 1回のクエリ: すべてのユーザーを取得 List<User> users = userRepository.findAll(); // SQL: SELECT * FROM users
// トランザクション内でOrderにアクセス for (User user : users) { // 各ユーザーごとにOrderを取得するクエリが実行される(N+1問題) List<Order> orders = user.getOrders(); // SQL: SELECT * FROM orders WHERE user_id = ? // これがユーザー数分(N回)実行される } }}N+1問題の解決方法
Section titled “N+1問題の解決方法”1. JOIN FETCHを使用
Section titled “1. JOIN FETCHを使用”@Repositorypublic interface UserRepository extends JpaRepository<User, Long> { // JOIN FETCHで関連エンティティを一度に取得 @Query("SELECT DISTINCT u FROM User u JOIN FETCH u.orders") List<User> findAllWithOrders();
// 特定のユーザーのみ取得する場合 @Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id") Optional<User> findByIdWithOrders(@Param("id") Long id);}
// 使用例@Servicepublic class UserService { public void displayAllUsers() { // 1回のクエリでユーザーと注文を同時に取得 List<User> users = userRepository.findAllWithOrders(); // SQL: SELECT u.*, o.* FROM users u // LEFT JOIN orders o ON u.id = o.user_id
// 追加のクエリは発生しない for (User user : users) { List<Order> orders = user.getOrders(); // 既にロード済み } }}2. @EntityGraphを使用
Section titled “2. @EntityGraphを使用”@Repositorypublic interface UserRepository extends JpaRepository<User, Long> { // @EntityGraphで関連エンティティを指定 @EntityGraph(attributePaths = {"orders"}) List<User> findAll();
@EntityGraph(attributePaths = {"orders", "orders.orderItems"}) Optional<User> findById(Long id);
// 複数の関連エンティティを指定 @EntityGraph(attributePaths = {"orders", "profile", "address"}) List<User> findByName(String name);}
// 使用例@Servicepublic class UserService { public void displayAllUsers() { // 1回のクエリでユーザーと注文を同時に取得 List<User> users = userRepository.findAll(); // SQL: SELECT u.*, o.* FROM users u // LEFT JOIN orders o ON u.id = o.user_id
// 追加のクエリは発生しない for (User user : users) { List<Order> orders = user.getOrders(); // 既にロード済み } }}3. 複数レベルの関連を取得
Section titled “3. 複数レベルの関連を取得”@Entitypublic class User { @OneToMany(fetch = FetchType.LAZY) private List<Order> orders;}
@Entitypublic class Order { @OneToMany(fetch = FetchType.LAZY) private List<OrderItem> orderItems;}
@Repositorypublic interface UserRepository extends JpaRepository<User, Long> { // 2レベル深い関連も一度に取得 @Query("SELECT DISTINCT u FROM User u " + "JOIN FETCH u.orders o " + "JOIN FETCH o.orderItems") List<User> findAllWithOrdersAndItems();
// @EntityGraphでも可能 @EntityGraph(attributePaths = {"orders", "orders.orderItems"}) List<User> findAll();}Eager LoadingとLazy Loadingの比較
Section titled “Eager LoadingとLazy Loadingの比較”| 項目 | Eager Loading | Lazy Loading |
|---|---|---|
| 取得タイミング | 親エンティティ取得時 | 関連エンティティアクセス時 |
| 初期クエリ | 複数のクエリが実行される可能性 | 1回のクエリ |
| 後続クエリ | 不要(既に取得済み) | 必要(アクセス時に実行) |
| トランザクション | トランザクション外でもアクセス可能 | トランザクション内でのみアクセス可能 |
| メモリ使用量 | 多い(すべて取得するため) | 少ない(必要な分だけ取得) |
| N+1問題 | 発生しやすい | 発生しやすい(適切な対策が必要) |
| 使用場面 | 常に必要な関連データ | 条件によって必要な関連データ |
実践的な使い分け
Section titled “実践的な使い分け”Eager Loadingが適している場合
Section titled “Eager Loadingが適している場合”// 1. 常に必要な関連データ@Entitypublic class Order { @ManyToOne(fetch = FetchType.EAGER) // 注文には常にユーザー情報が必要 private User user;}
// 2. 小さな関連データ@Entitypublic class User { @OneToOne(fetch = FetchType.EAGER) // プロフィールは常に必要で、サイズも小さい private UserProfile profile;}Lazy Loadingが適している場合
Section titled “Lazy Loadingが適している場合”// 1. 大きなコレクション@Entitypublic class User { @OneToMany(fetch = FetchType.LAZY) // 注文は大量にある可能性がある private List<Order> orders;}
// 2. 条件によって必要なデータ@Entitypublic class Product { @OneToMany(fetch = FetchType.LAZY) // レビューは必要な時だけ取得 private List<Review> reviews;}パフォーマンス最適化のテクニック
Section titled “パフォーマンス最適化のテクニック”1. バッチサイズの設定
Section titled “1. バッチサイズの設定”@Entitypublic class User { @OneToMany(fetch = FetchType.LAZY) @BatchSize(size = 50) // 50件ずつバッチで取得 private List<Order> orders;}
// application.propertiesspring.jpa.properties.hibernate.jdbc.batch_size=50動作:
// 100人のユーザーを取得List<User> users = userRepository.findAll();// SQL: SELECT * FROM users
// 最初のユーザーの注文にアクセスusers.get(0).getOrders();// SQL: SELECT * FROM orders WHERE user_id IN (1, 2, 3, ..., 50)// 50件ずつバッチで取得される2. サブセレクトの使用
Section titled “2. サブセレクトの使用”@Entitypublic class User { @OneToMany(fetch = FetchType.LAZY) @Fetch(FetchMode.SUBSELECT) // サブクエリで一括取得 private List<Order> orders;}動作:
List<User> users = userRepository.findAll();// SQL: SELECT * FROM users
users.get(0).getOrders();// SQL: SELECT * FROM orders// WHERE user_id IN (SELECT id FROM users)// すべてのユーザーの注文を一度に取得3. 動的なフェッチ戦略
Section titled “3. 動的なフェッチ戦略”@Repositorypublic interface UserRepository extends JpaRepository<User, Long> { // デフォルトはLAZYだが、必要に応じてEAGERで取得 @EntityGraph(attributePaths = {"orders"}) Optional<User> findByIdWithOrders(Long id);
// 通常の取得(LAZY) Optional<User> findById(Long id);}
// 使用例@Servicepublic class UserService { public UserDTO getUserBasic(Long id) { // 注文情報は不要な場合 User user = userRepository.findById(id).orElseThrow(); return convertToBasicDTO(user); }
public UserDTO getUserWithOrders(Long id) { // 注文情報も必要な場合 User user = userRepository.findByIdWithOrders(id).orElseThrow(); return convertToFullDTO(user); }}よくある問題と解決策
Section titled “よくある問題と解決策”問題1: LazyInitializationException
Section titled “問題1: LazyInitializationException”// 問題のコード@Servicepublic class UserService { public UserDTO getUser(Long id) { User user = userRepository.findById(id).orElseThrow(); // トランザクションが終了
// トランザクション外でLazy Loadingを試みる List<Order> orders = user.getOrders(); // LazyInitializationExceptionが発生 }}解決策1: @Transactionalを使用
@Service@Transactional(readOnly = true)public class UserService { public UserDTO getUser(Long id) { User user = userRepository.findById(id).orElseThrow(); // トランザクション内でLazy Loading List<Order> orders = user.getOrders(); // OK return convertToDTO(user, orders); }}解決策2: JOIN FETCHを使用
@Servicepublic class UserService { public UserDTO getUser(Long id) { // JOIN FETCHで一度に取得 User user = userRepository.findByIdWithOrders(id).orElseThrow(); // 既にロード済みなので、トランザクション外でもアクセス可能 List<Order> orders = user.getOrders(); // OK return convertToDTO(user, orders); }}問題2: 過度なEager Loading
Section titled “問題2: 過度なEager Loading”// 問題のコード: すべてEAGERに設定@Entitypublic class User { @OneToMany(fetch = FetchType.EAGER) private List<Order> orders;
@OneToMany(fetch = FetchType.EAGER) private List<Address> addresses;
@OneToMany(fetch = FetchType.EAGER) private List<Review> reviews;}
// Userを1件取得するだけで、大量のデータが取得されるUser user = userRepository.findById(1L).orElseThrow();// 実行されるSQL:// SELECT * FROM users WHERE id = 1// SELECT * FROM orders WHERE user_id = 1// SELECT * FROM addresses WHERE user_id = 1// SELECT * FROM reviews WHERE user_id = 1解決策: 必要に応じて動的に取得
@Entitypublic class User { // すべてLAZYに設定 @OneToMany(fetch = FetchType.LAZY) private List<Order> orders;
@OneToMany(fetch = FetchType.LAZY) private List<Address> addresses;
@OneToMany(fetch = FetchType.LAZY) private List<Review> reviews;}
@Repositorypublic interface UserRepository extends JpaRepository<User, Long> { // 必要に応じてJOIN FETCH @Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id") Optional<User> findByIdWithOrders(@Param("id") Long id);
@Query("SELECT u FROM User u JOIN FETCH u.addresses WHERE u.id = :id") Optional<User> findByIdWithAddresses(@Param("id") Long id);}ベストプラクティス
Section titled “ベストプラクティス”1. デフォルトはLAZY、必要に応じてEAGER
Section titled “1. デフォルトはLAZY、必要に応じてEAGER”@Entitypublic class User { // デフォルトでLAZY(推奨) @OneToMany(fetch = FetchType.LAZY) private List<Order> orders;
// 常に必要な場合はEAGER @OneToOne(fetch = FetchType.EAGER) private UserProfile profile;}2. JOIN FETCHや@EntityGraphを積極的に使用
Section titled “2. JOIN FETCHや@EntityGraphを積極的に使用”// 必要な関連データを明示的に指定@Repositorypublic interface UserRepository extends JpaRepository<User, Long> { @EntityGraph(attributePaths = {"orders"}) List<User> findAll();}3. バッチサイズの設定
Section titled “3. バッチサイズの設定”// コレクションに@BatchSizeを設定@OneToMany(fetch = FetchType.LAZY)@BatchSize(size = 50)private List<Order> orders;4. パフォーマンステストの実施
Section titled “4. パフォーマンステストの実施”@Testpublic void testQueryPerformance() { long startTime = System.currentTimeMillis();
List<User> users = userRepository.findAll(); for (User user : users) { user.getOrders().size(); // Lazy Loadingをトリガー }
long duration = System.currentTimeMillis() - startTime; assertThat(duration).isLessThan(1000); // 1秒以内に完了すること}Eager LoadingとLazy Loadingの使い分け:
- Eager Loading: 常に必要な小さな関連データに使用
- Lazy Loading: 大きなコレクションや条件によって必要なデータに使用(デフォルト推奨)
- N+1問題の解決: JOIN FETCHや@EntityGraphを使用
- パフォーマンス最適化: バッチサイズの設定、動的なフェッチ戦略
適切なフェッチ戦略を選択することで、パフォーマンスを最適化し、N+1問題を回避できます。