OSIV (Open Session In View)

### OSIV (Open Session In View) の概念と役割

**OSIV (Open Session In View)** とは、**Spring JPA** で使用されるパターンで、永続コンテキスト（Session または EntityManager）を **ビュー(View)** がレンダリングされるまで **開いたままにする** 方法です。このパターンは **遅延読み込み (Lazy Loading)** をサポートするために使用され、永続コンテキストを Web リクエスト全体（コントローラー、サービス、ビュー）にわたって維持し、**エンティティの遅延ロードされたフィールド** にビューからアクセスできるようにします。

### OSIVの基本動作

1. **OSIV が有効** な場合、トランザクションが終了した後もデータベースセッション（永続コンテキスト）が開いたままであり、**コントローラーやビュー層** でもデータベースにアクセスして遅延読み込みを処理できます。
2. **OSIV が無効** な場合、永続コンテキストは **サービス層でトランザクションが終了した時点で閉じられる** ため、トランザクション終了後に **遅延読み込みされたフィールド** にアクセスすると **`LazyInitializationException`** が発生します。

### OSIVのメリット

1. **遅延読み込みのサポート**: 遅延ロードされたエンティティに **ビューやコントローラー** から自由にアクセスできます。OSIV が有効な状態では、サービス層でトランザクションが終了した後でも、ビューのレンダリング中にエンティティの遅延ロードフィールドをデータベースからロードできます。

2. **開発の利便性**: 永続コンテキストがビューのレンダリングまで維持されるため、コントローラーやビューからエンティティ間の関係を簡単に操作できます。

### OSIVのデメリット

1. **データベース接続の長期占有**: OSIV が有効な場合、リクエストが完了するまでデータベースセッションを維持するため、**データベース接続を長時間占有** する可能性があります。特に **同時リクエスト** が多い環境では、パフォーマンス低下を引き起こす可能性があります。

2. **パフォーマンスの低下**: ビューで無計画に **遅延読み込み** を使用すると、必要のない時点で多数のデータベースクエリが発生する可能性があります。これにより、**N+1問題** などのパフォーマンス上の問題が生じることがあります。

3. **トランザクション境界が不明確**: 通常、トランザクションは **サービス層で終了する** べきですが、OSIV が有効な場合、ビューでもデータベースにアクセスできるため、**トランザクションの境界** が不明確になる可能性があります。

### OSIVの無効化

**OSIV の無効化** は、パフォーマンスやデータベースリソース管理の観点から、**高パフォーマンスのシステム** でよく使用されます。OSIV を無効にすると、永続コンテキストは **トランザクションが終了すると直ちに閉じられ**、ビュー層ではデータベースにアクセスできなくなります。そのため、**遅延読み込みされたフィールドにアクセスするには**、サービス層で事前にデータをロードしておく必要があります。

#### OSIV の無効化方法 (Spring Boot)

`application.properties` または `application.yml` で次のように設定して、OSIV を無効にできます。

```properties
spring.jpa.open-in-view=false
```

### OSIV の無効化時に発生する問題

1. **`LazyInitializationException`**: サービス層で永続コンテキストが閉じられた後、コントローラーやビューで遅延ロードされたエンティティにアクセスすると **`LazyInitializationException`** が発生します。たとえば、サービスで `Order` エンティティを取得した後、コントローラーで `Order` に関連する `OrderItem` を遅延読み込みしようとすると、このエラーが発生します。

**エラーの例**:

```text
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: Order.items, could not initialize proxy - no Session
```

### OSIV 無効化時の解決方法

OSIV を無効にした状態で **遅延読み込み問題** を解決するには、**サービス層で必要なデータを事前にロード** する戦略を使用する必要があります。

#### 1. **フェッチ結合 (Fetch Join)** を使用

サービス層で **JPQL** の `JOIN FETCH` を使用して必要な関連エンティティを **即時読み込み** します。

```java
@Service
public class OrderService {

    @PersistenceContext
    private EntityManager em;

    @Transactional(readOnly = true)
    public Order findOrderByIdWithItems(Long id) {
        return em.createQuery("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id", Order.class)
                 .setParameter("id", id)
                 .getSingleResult();
    }
}
```

#### 2. **`@EntityGraph`** を使用

JPA の `@EntityGraph` アノテーションを使用して、特定のエンティティを取得する際に **即時読み込み** されるように設定します。

```java
public interface OrderRepository extends JpaRepository<Order, Long> {

    @EntityGraph(attributePaths = "items")
    Optional<Order> findById(Long id);
}
```

#### 3. **DTO（Data Transfer Object）パターン** を使用

サービス層でエンティティを **DTO に変換** してコントローラーやビューに渡すことで、エンティティの遅延読み込み問題を完全に回避できます。

```java
public class OrderDTO {
    private Long id;
    private String customerName;
    private List<OrderItemDTO> items;
    // Getters, setters, constructors
}
```

サービスで **DTO** に変換:

```java
@Service
public class OrderService {

    public OrderDTO findOrderById(Long id) {
        Order order = orderRepository.findById(id).orElseThrow(() -> new RuntimeException("Order not found"));

        List<OrderItemDTO> items = order.getItems().stream()
            .map(item -> new OrderItemDTO(item.getId(), item.getProductName(), item.getQuantity()))
            .collect(Collectors.toList());

        return new OrderDTO(order.getId(), order.getCustomerName(), items);
    }
}
```
### Example Code

application.properties

```
spring.application.name=jpaStudy
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.open-in-view=false
spring.jpa.hibernate.ddl-auto=update

```

orderController

``` java

@RestController
public class OrderController {
    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/orders")
    public List<OrderDto> listOrders() {
        return orderService.findAllOrdersWithItems();
    }

    @GetMapping("/orders/{id}")
    public Order getOrderDetails(@PathVariable Long id){
        Optional<Order> order = orderService.findOrderById(id);
        return order.orElse(null);
    }

    @GetMapping("/order")
    public String createSampleOrder() {
        orderService.createSampleOrder(); // Create a sample order
        return "created";
    }
}

```


orderService

``` java
import jakarta.transaction.Transactional;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
public class OrderService {
    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Transactional
    public List<OrderDto> findAllOrdersWithItems() {
        // Using EntityGraph to fetch orders with items
        List<Order> orders = orderRepository.findAll();
        return orders.stream()
                .map(order -> new OrderDto(order.getId(), order.getCustomerName(),
                        order.getItems().stream()
                                .map(item -> new OrderItemDto(item.getId(), item.getProductName(), item.getQuantity()))
                                .collect(Collectors.toList())))
                .collect(Collectors.toList());
    }

    @Transactional
    public void createSampleOrder() {
        Order order = new Order();
        order.setCustomerName("John Doe");

        OrderItem item1 = new OrderItem();
        item1.setProductName("Laptop");
        item1.setQuantity(1);
        item1.setOrder(order);

        OrderItem item2 = new OrderItem();
        item2.setProductName("Mouse");
        item2.setQuantity(2);
        item2.setOrder(order);

        order.getItems().add(item1);
        order.getItems().add(item2);

        orderRepository.save(order);
    }

    public Optional<Order> findOrderById(Long id) {
        return orderRepository.findById(id);
    }
}

```

OrderRepository

``` java
public interface OrderRepository  extends JpaRepository<Order, Long> {

    @EntityGraph(attributePaths = "items")
    List<Order> findAll();
}

```

Entity

``` java

@Entity
@Data
@Table(name = "custom_order")
public class Order {

    @Id @GeneratedValue
    private Long id;

    private String customerName;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> items = new ArrayList<>();
}

@Entity
@Data
public class OrderItem {

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

    private String productName;
    private int quantity;

    @ManyToOne
    @JoinColumn(name = "order_id")
    @JsonIgnore
    private Order order;

}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderDto {
    private Long id;
    private String customerName;
    private List<OrderItemDto> items;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderItemDto {
    private Long id;
    private String productName;
    private int quantity;
}

```
### Error Message

```
2024-09-28T22:05:01.891+09:00  WARN 8264 --- [jpaStudy] [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: failed to lazily initialize a collection of role: dev.rumblekat.jpa.entity.Order.items: could not initialize proxy - no Session]
```

### 結論

1. **OSIV** は遅延読み込みをサポートし、開発の利便性を高めますが、**パフォーマンスの低下** や **データベース接続の長期占有** などのデメリットがあります。
2. **OSIV を無効にする** と、**`LazyInitializationException`** を回避するために、サービス層で **フェッチ結合** や **`@EntityGraph`** を使用して必要なデータを事前にロードするか、**DTO パターン** を使用することが一般的な解決策です。
3. 高パフォーマンスのシステムでは **OSIV の無効化** が望ましく、データロード戦略を明確に定義して管理することが重要です。