0
0

OSIV (Open Session In View)

Posted at

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 を無効にできます。

spring.jpa.open-in-view=false

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

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

エラーの例:

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

OSIV 無効化時の解決方法

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

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

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

@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 アノテーションを使用して、特定のエンティティを取得する際に 即時読み込み されるように設定します。

public interface OrderRepository extends JpaRepository<Order, Long> {

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

3. DTO(Data Transfer Object)パターン を使用

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

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

サービスで DTO に変換:

@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


@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

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

public interface OrderRepository  extends JpaRepository<Order, Long> {

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

Entity


@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 の無効化 が望ましく、データロード戦略を明確に定義して管理することが重要です。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0