logo Fonnpo

お問い合わせ
2026年5月30日 4 分で読める Fonnpo

Spring フレームワーク完全ガイド

Spring は Java エコシステムで最も人気のあるアプリケーションフレームワークです。本記事では IoC と DI の核心思想、Bean のライフサイクルとスコープ、AOP、Spring MVC のリクエスト処理、Spring Boot の自動設定、トランザクションの伝播と分離レベル、Spring Data JPA、そして循環依存などの落とし穴を体系的に解説します。

#Java#Spring#Backend

Spring は Java エコシステムで最も人気のあるアプリケーションフレームワークであり、IoC コンテナ、AOP、MVC、トランザクション管理などの機能を提供し、エンタープライズアプリケーション開発を大幅に簡略化します。

Spring コアモジュール

Spring フレームワークは複数のモジュールで構成されており、必要に応じて導入できます:

モジュール説明
spring-coreIoC コンテナのコア基盤
spring-beansBeanFactory — Bean の生成と管理を担当
spring-contextApplicationContext — beans を基盤に、より豊富な機能を提供
spring-aopアスペクト指向プログラミングのサポート
spring-webmvcSpring MVC フレームワーク
spring-txトランザクション管理
spring-data-jpaJPA 統合
spring-boot設定より規約、自動設定

IoC(制御の反転)と DI(依存性注入)

IoC とは

従来のモデルでは、オブジェクトは自身の依存オブジェクトを自ら生成します:

Java
        public class UserService {
    // 自分で new — 結合度が高い
    private UserRepository repo = new UserRepositoryImpl();
}

    

IoC(Inversion of Control)は、オブジェクトの生成と依存関係の管理を Spring コンテナに委ねます。オブジェクトは必要なものを宣言するだけで、コンテナがそれを注入します:

Java
        @Service
public class UserService {
    private final UserRepository repo;

    // Spring が自動的に注入する
    public UserService(UserRepository repo) {
        this.repo = repo;
    }
}

    

制御がオブジェクト自身からコンテナへ「反転」する — これが IoC の核心思想です。

DI の 3 つの方法

1. コンストラクタ注入(推奨)

Java
        @Service
public class OrderService {
    private final UserRepository userRepo;
    private final ProductRepository productRepo;

    // コンストラクタが 1 つの場合、@Autowired は省略可
    public OrderService(UserRepository userRepo, ProductRepository productRepo) {
        this.userRepo = userRepo;
        this.productRepo = productRepo;
    }
}

    

利点:依存を final として宣言でき、不変性が保たれ、テストしやすく、オブジェクト構築時点で依存が確実に揃います。

2. セッター注入

Java
        @Service
public class NotificationService {
    private EmailSender emailSender;

    @Autowired
    public void setEmailSender(EmailSender emailSender) {
        this.emailSender = emailSender;
    }
}

    

省略可能な依存や、実行時に実装を動的に切り替える必要があるシナリオに適しています。

3. フィールド注入(非推奨)

Java
        @Service
public class UserService {
    @Autowired
    private UserRepository userRepo; // final 宣言不可、単体テストが困難
}

    

コードは簡潔ですが final が使えず、コンテナ外でインスタンス化すると依存が null になるため、本番コードでの使用は推奨しません。

Bean 管理

Bean の定義方法

アノテーション方式(最も一般的)

Java
        @Component        // 汎用コンポーネント
@Service          // ビジネス層
@Repository       // データアクセス層(JPA 例外変換も有効化)
@Controller       // MVC コントローラ
@RestController   // = @Controller + @ResponseBody

    

Java Config 方式

Java
        @Configuration
public class AppConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public EmailSender emailSender() {
        return new SmtpEmailSender();
    }
}

    

XML 方式(レガシープロジェクトで引き続き使用)

Xml
        <bean id="userService" class="com.example.UserService">
    <constructor-arg ref="userRepository"/>
</bean>

    

Bean のスコープ

Java
        @Component
@Scope("singleton")   // デフォルト — コンテナ内にインスタンスは 1 つだけ
public class AppConfig { }

@Component
@Scope("prototype")   // getBean() のたびに新しいインスタンスを生成
public class RequestProcessor { }

// Web 環境専用
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class RequestContext { }

@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class UserSession { }

    
スコープ説明
singletonデフォルト — コンテナ内で唯一のインスタンス
prototype注入 / getBean() のたびに新しいインスタンスを生成
requestHTTP リクエストごとに 1 インスタンス(Web 環境)
sessionHTTP セッションごとに 1 インスタンス(Web 環境)

Bean のライフサイクル

Properties
        インスタンス化
  → プロパティ注入
  → BeanNameAware / BeanFactoryAware / ApplicationContextAware
  → BeanPostProcessor#postProcessBeforeInitialization
  → @PostConstruct
  → InitializingBean#afterPropertiesSet
  → init-method
  → BeanPostProcessor#postProcessAfterInitialization
  → 【Bean 使用可能】
  → @PreDestroy
  → DisposableBean#destroy
  → destroy-method
  → 破棄

    

よく使われるライフサイクルフック:

Java
        @Component
public class DataSourcePool {

    @PostConstruct
    public void init() {
        // Bean の初期化完了後に実行 — コネクションプールのウォームアップに最適
        System.out.println("コネクションプール初期化完了");
    }

    @PreDestroy
    public void destroy() {
        // コンテナのシャットダウン前に実行 — リソース解放に最適
        System.out.println("コネクションプールリソースを解放");
    }
}

    

よく使われるアノテーション

注入と設定

Java
        // 型による自動注入
@Autowired
private UserRepository userRepo;

// 同じ型の Bean が複数ある場合、名前で指定
@Autowired
@Qualifier("mysqlUserRepo")
private UserRepository userRepo;

// @Autowired + @Qualifier と同等、よりシンプル(JSR-250 標準)
@Resource(name = "mysqlUserRepo")
private UserRepository userRepo;

// 設定ファイルの値を注入
@Value("${app.name}")
private String appName;

// デフォルト値付きで注入
@Value("${app.timeout:30}")
private int timeout;

// SpEL 式を注入
@Value("#{systemProperties['user.home']}")
private String userHome;

    

設定クラスのアノテーション

Java
        @Configuration                             // 設定クラスを示す
@ComponentScan("com.example")              // スキャン対象のパッケージを指定
@PropertySource("classpath:app.properties") // 追加のプロパティファイルを読み込む
@Import(SecurityConfig.class)              // 別の設定クラスをインポート
public class AppConfig { }

    

条件付きアセンブリ(Spring Boot でよく使用)

Java
        // プロパティが存在し、指定された値の場合のみアセンブル
@ConditionalOnProperty(name = "feature.cache.enabled", havingValue = "true")
@Bean
public CacheManager cacheManager() {
    return new RedisCacheManager(...);
}

// 指定した型の Bean が存在しない場合のみアセンブル(デフォルト実装を提供)
@ConditionalOnMissingBean(DataSource.class)
@Bean
public DataSource embeddedDataSource() {
    return new EmbeddedDatabaseBuilder().build();
}

// 指定したクラスがクラスパス上にある場合のみアセンブル
@ConditionalOnClass(RedisTemplate.class)
@Configuration
public class RedisAutoConfig { }

    

AOP(アスペクト指向プログラミング)

AOP を使うと、ビジネスコードを変更せずに、ロギング、認可チェック、パフォーマンス監視、トランザクションなどの横断的関心事を一元管理できます。

コアコンセプト

コンセプト説明
Aspect(アスペクト)横断的ロジックをカプセル化したクラス。@Aspect でアノテート
Pointcut(ポイントカット)どのメソッドに適用するかを定義する式
Advice(アドバイス)ポイントカットがトリガーされたときに実行されるコード
JoinPoint(ジョインポイント)インターセプト可能な実行位置(メソッド呼び出し、例外など)
Weaving(ウィービング)アスペクトのロジックをターゲットオブジェクトに適用するプロセス

アドバイスの種類

Java
        @Aspect
@Component
public class LoggingAspect {

    // ポイントカット式 — service パッケージ配下の全メソッドにマッチ
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethods() {}

    // メソッド実行前
    @Before("serviceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("メソッド呼び出し:" + joinPoint.getSignature().getName());
    }

    // メソッド実行後(例外の有無に関わらず)
    @After("serviceMethods()")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("メソッド実行完了");
    }

    // 正常にリターンした後
    @AfterReturning(pointcut = "serviceMethods()", returning = "result")
    public void logAfterReturning(Object result) {
        System.out.println("戻り値:" + result);
    }

    // 例外がスローされた後
    @AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
    public void logAfterThrowing(Exception ex) {
        System.out.println("例外:" + ex.getMessage());
    }

    // ラウンドアドバイス(最も強力 — メソッドを実行するかどうかを制御できる)
    @Around("serviceMethods()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            Object result = pjp.proceed(); // ターゲットメソッドを実行
            long elapsed = System.currentTimeMillis() - start;
            System.out.println(pjp.getSignature() + " 処理時間:" + elapsed + "ms");
            return result;
        } catch (Exception e) {
            System.out.println("例外発生:" + e.getMessage());
            throw e;
        }
    }
}

    
折りたたむ

ポイントカット式

Java
        // 特定のメソッドに厳密にマッチ
execution(public String com.example.service.UserService.findById(Long))

// クラス内の全メソッドにマッチ
execution(* com.example.service.UserService.*(..))

// パッケージとそのサブパッケージ内の全メソッドにマッチ(.. は任意のサブパッケージ)
execution(* com.example.service..*.*(..))

// 特定のアノテーションが付いたメソッドにマッチ
@annotation(com.example.annotation.Log)

// パッケージ内の全型にマッチ
within(com.example.service.*)

// 組み合わせ(get で始まるメソッドを除外)
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.*.get*(..))")

    

AOP 実践:統一操作ログ

Java
        // カスタムアノテーション
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
    String value() default "";
}

// アスペクト実装
@Aspect
@Component
public class OperationLogAspect {

    @Autowired
    private LogService logService;

    @Around("@annotation(operationLog)")
    public Object record(ProceedingJoinPoint pjp, OperationLog operationLog) throws Throwable {
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        String operation = operationLog.value();

        try {
            Object result = pjp.proceed();
            logService.save(username, operation, "SUCCESS");
            return result;
        } catch (Exception e) {
            logService.save(username, operation, "FAILED: " + e.getMessage());
            throw e;
        }
    }
}

// アノテーションの使用
@Service
public class UserService {
    @OperationLog("ユーザー削除")
    public void deleteUser(Long id) {
        // ビジネスコードはログを意識しなくてよい
    }
}

    
折りたたむ

Spring MVC

リクエスト処理フロー

Properties
        クライアントリクエスト
  → DispatcherServlet(フロントコントローラ)
  → HandlerMapping(対応する Controller メソッドを検索)
  → HandlerAdapter(Controller メソッドを実行)
  → ModelAndView または @ResponseBody データを返す
  → ViewResolver(ビュー名の場合、対応するビューを検索)
  → レンダリング → クライアントへレスポンス

    

Controller のよく使うアノテーション

Java
        @RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserService userService;

    // GET /api/users
    @GetMapping
    public List<User> list() {
        return userService.findAll();
    }

    // GET /api/users/1
    @GetMapping("/{id}")
    public ResponseEntity<User> getById(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    // POST /api/users
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public User create(@RequestBody @Valid CreateUserRequest req) {
        return userService.create(req);
    }

    // PUT /api/users/1
    @PutMapping("/{id}")
    public User update(@PathVariable Long id, @RequestBody @Valid UpdateUserRequest req) {
        return userService.update(id, req);
    }

    // DELETE /api/users/1
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        userService.delete(id);
    }

    // GET /api/users?page=0&size=10&keyword=Alice
    @GetMapping("/search")
    public Page<User> search(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(required = false) String keyword
    ) {
        return userService.search(keyword, PageRequest.of(page, size));
    }
}

    
折りたたむ

パラメータバインディング

Java
        // パスパラメータ
@GetMapping("/{id}")
public User get(@PathVariable Long id) { ... }

// クエリパラメータ(デフォルト値付き)
@GetMapping
public List<User> list(
    @RequestParam String name,
    @RequestParam(defaultValue = "0") int page
) { ... }

// リクエストボディ(JSON → オブジェクト)
@PostMapping
public User create(@RequestBody CreateUserRequest req) { ... }

// リクエストヘッダ
@GetMapping
public String auth(@RequestHeader("Authorization") String token) { ... }

// Cookie
@GetMapping
public String session(@CookieValue("sessionId") String sid) { ... }

// クエリパラメータをオブジェクトに自動バインド(?name=Alice&age=25 → UserSearchRequest)
@GetMapping
public Page<User> search(UserSearchRequest req) { ... }

    

グローバル例外ハンドリング

Java
        @RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleNotFound(EntityNotFoundException ex) {
        return new ErrorResponse("NOT_FOUND", ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult().getFieldErrors()
            .stream()
            .map(f -> f.getField() + ": " + f.getDefaultMessage())
            .collect(Collectors.toList());
        return new ErrorResponse("VALIDATION_FAILED", errors.toString());
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleGeneral(Exception ex) {
        return new ErrorResponse("INTERNAL_ERROR", "サーバー内部エラー");
    }
}

    

Spring Boot

自動設定の仕組み

Spring Boot は @SpringBootApplication によって自動設定を有効にします。これは 3 つのアノテーションの組み合わせです:

Java
        @SpringBootConfiguration   // @Configuration と同等
@EnableAutoConfiguration   // コア:自動設定を有効化
@ComponentScan             // 現在のパッケージとサブパッケージをスキャン
public @interface SpringBootApplication { }

    

@EnableAutoConfigurationAutoConfigurationImportSelector を通じて META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports に列挙された全自動設定クラスを読み込み、各 @ConditionalOnXxx アノテーションで条件を評価して Bean を選択的に登録します。

フロー全体を簡略化すると:

Properties
        起動
  → AutoConfiguration.imports をスキャン
  → 各 @ConditionalOnXxx 条件をチェック
  → 条件を満たす → Bean を登録
  → 条件を満たさない → スキップ

    

設定ファイル

Yaml
        # application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: secret
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false

server:
  port: 8080
  servlet:
    context-path: /api

app:
  jwt:
    secret: my-secret-key
    expiration: 86400

    

マルチ環境設定

Yaml
        # application.yml
spring:
  profiles:
    active: dev # デフォルトで dev 環境を有効化

---
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:h2:mem:testdb

---
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: jdbc:mysql://prod-server:3306/mydb

    

設定をクラスにバインド

Java
        @ConfigurationProperties(prefix = "app.jwt")
@Component
public class JwtProperties {
    private String secret;
    private long expiration;
    // getters & setters
}

    

トランザクション管理

基本的な使い方

Java
        @Service
public class TransferService {

    @Transactional
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepo.findById(fromId).orElseThrow();
        Account to   = accountRepo.findById(toId).orElseThrow();

        from.deduct(amount);
        to.add(amount);

        accountRepo.save(from);
        accountRepo.save(to);
        // メソッドが正常終了 → 自動コミット
        // RuntimeException がスロー → 自動ロールバック
    }
}

    

トランザクション伝播

Java
        // REQUIRED(デフォルト):既存トランザクションに参加、なければ新規作成
@Transactional(propagation = Propagation.REQUIRED)

// REQUIRES_NEW:常に新規トランザクションを作成し、現在のトランザクションをサスペンド
@Transactional(propagation = Propagation.REQUIRES_NEW)

// SUPPORTS:既存トランザクションに参加、なければ非トランザクションで実行
@Transactional(propagation = Propagation.SUPPORTS)

// NOT_SUPPORTED:非トランザクションで実行、現在のトランザクションがあればサスペンド
@Transactional(propagation = Propagation.NOT_SUPPORTED)

// NEVER:トランザクション内での実行を禁止、あれば例外をスロー
@Transactional(propagation = Propagation.NEVER)

// MANDATORY:トランザクション内での実行が必須、なければ例外をスロー
@Transactional(propagation = Propagation.MANDATORY)

// NESTED:ネストされたトランザクション(セーブポイント使用)。外側のロールバックで内側もロールバック、内側のロールバックは外側に影響しない
@Transactional(propagation = Propagation.NESTED)

    

分離レベル

分離レベルダーティリード反復不能読み取りファントムリード
READ_UNCOMMITTED✅ あり✅ あり✅ あり
READ_COMMITTED❌ なし✅ あり✅ あり
REPEATABLE_READ❌ なし❌ なし✅ あり
SERIALIZABLE❌ なし❌ なし❌ なし
Java
        @Transactional(isolation = Isolation.READ_COMMITTED)   // Oracle のデフォルト
@Transactional(isolation = Isolation.REPEATABLE_READ)  // MySQL InnoDB のデフォルト
@Transactional(isolation = Isolation.SERIALIZABLE)     // 最も厳格、パフォーマンスが最低

    

注意事項

Java
        @Service
public class UserService {

    // ❌ 自己呼び出しではトランザクションが発動しない(this はプロキシではないため AOP が機能しない)
    public void outer() {
        this.inner();
    }

    @Transactional
    public void inner() { ... }

    // ✅ コンテナからプロキシオブジェクトを取得して呼び出す
    @Autowired
    private ApplicationContext context;

    public void outer2() {
        context.getBean(UserService.class).inner();
    }

    // ❌ デフォルトでは RuntimeException のみロールバック、チェック例外はロールバックしない
    @Transactional
    public void riskyOp() throws IOException {
        // IOException はデフォルトでロールバックしない
    }

    // ✅ 全例外でロールバックするよう明示的に指定
    @Transactional(rollbackFor = Exception.class)
    public void safeOp() throws IOException { ... }

    // ❌ private メソッドの @Transactional は無効(AOP は private メソッドをプロキシできない)
    @Transactional
    private void internalUpdate() { ... }
}

    
折りたたむ

Spring Data JPA

Repository 層

Java
        @Repository
public interface UserRepository extends JpaRepository<User, Long> {

    // メソッド命名規則 — Spring が実装を自動生成
    List<User> findByEmail(String email);
    List<User> findByAgeGreaterThan(int age);
    Optional<User> findByEmailAndStatus(String email, UserStatus status);
    boolean existsByEmail(String email);
    long countByStatus(UserStatus status);

    // ページングとソート
    Page<User> findByStatus(UserStatus status, Pageable pageable);
    List<User> findAll(Sort sort);

    // カスタム JPQL
    @Query("SELECT u FROM User u WHERE u.name LIKE %:keyword% OR u.email LIKE %:keyword%")
    List<User> search(@Param("keyword") String keyword);

    // ネイティブ SQL
    @Query(value = "SELECT * FROM users WHERE created_at > :date", nativeQuery = true)
    List<User> findCreatedAfter(@Param("date") LocalDate date);

    // 更新 / 削除操作には @Modifying が必要
    @Modifying
    @Query("UPDATE User u SET u.status = :status WHERE u.id IN :ids")
    int batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") UserStatus status);
}

    

エンティティ定義

Java
        @Entity
@Table(name = "users", indexes = {
    @Index(name = "idx_email", columnList = "email", unique = true)
})
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 50)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    @Enumerated(EnumType.STRING)
    private UserStatus status;

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    // 遅延ロード — N+1 問題を回避
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Order> orders;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "department_id")
    private Department department;
}

    
折りたたむ

よくある問題とベストプラクティス

循環依存

Spring Boot 2.6+ はデフォルトで循環依存を禁止しており、検出された場合は BeanCurrentlyInCreationException をスローします。

Java
        // ❌ 循環依存(A は B に依存し、B は A に依存する)
@Service class A { @Autowired B b; }
@Service class B { @Autowired A a; }

// ✅ 解決策 1:設計を見直し、共通ロジックを新しいクラス C に抽出(推奨)
@Service class C { /* 共通ロジック */ }
@Service class A { @Autowired C c; }
@Service class B { @Autowired C c; }

// ✅ 解決策 2:@Lazy 遅延注入で初期化のデッドロックを回避
@Service class A {
    @Autowired @Lazy B b;
}

// ✅ 解決策 3:セッター注入(根本的な解決ではなく回避策のため非推奨)
@Service class A {
    private B b;
    @Autowired
    public void setB(B b) { this.b = b; }
}

    

複数の Bean から選択する

Java
        // 2 つの UserRepository 実装
@Component("mysqlUserRepo")
public class MysqlUserRepository implements UserRepository { ... }

@Component("redisUserRepo")
public class RedisUserRepository implements UserRepository { ... }

// 方法 1:@Qualifier で名前を指定
@Autowired
@Qualifier("mysqlUserRepo")
private UserRepository userRepo;

// 方法 2:@Primary でデフォルト実装をマーク
@Primary
@Component
public class MysqlUserRepository implements UserRepository { ... }

// 方法 3:全実装を注入して自分で選択
@Autowired
private List<UserRepository> allRepos;

@Autowired
private Map<String, UserRepository> repoMap; // キーは bean 名

    

よくある落とし穴

Java
        // 1. private メソッドの @Transactional は無効
@Transactional        // ❌ 無効 — AOP は private メソッドをプロキシできない
private void internalUpdate() { ... }

// 2. static メソッドのアノテーションは無効(AOP はインスタンスプロキシに依存)
@Cacheable("users")   // ❌ 無効
public static User staticGet(Long id) { ... }

// 3. prototype Bean を singleton に注入すると常に同じインスタンスが返る
// ✅ @Lookup を使うと Spring が毎回呼び出し時に新しい prototype を生成する
@Component
public class SingletonBean {
    @Lookup
    public PrototypeBean getPrototype() {
        return null; // Spring がこのメソッドをオーバーライドし、毎回新しいインスタンスを返す
    }
}

// 4. Session クローズ後に JPA の遅延ロード関連にアクセスすると LazyInitializationException
// ✅ 解決策:Service 層で @Transactional を使って Session を維持するか、JOIN FETCH を使用
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
Optional<User> findByIdWithOrders(@Param("id") Long id);

    
Fonnpo