OSIV (Open Session In View) の概念と役割
OSIV (Open Session In View) とは、Spring JPA で使用されるパターンで、永続コンテキスト(Session または EntityManager)を ビュー(View) がレンダリングされるまで 開いたままにする 方法です。このパターンは 遅延読み込み (Lazy Loading) をサポートするために使用され、永続コンテキストを Web リクエスト全体(コントローラー、サービス、ビュー)にわたって維持し、エンティティの遅延ロードされたフィールド にビューからアクセスできるようにします。
OSIVの基本動作
- OSIV が有効 な場合、トランザクションが終了した後もデータベースセッション(永続コンテキスト)が開いたままであり、コントローラーやビュー層 でもデータベースにアクセスして遅延読み込みを処理できます。
-
OSIV が無効 な場合、永続コンテキストは サービス層でトランザクションが終了した時点で閉じられる ため、トランザクション終了後に 遅延読み込みされたフィールド にアクセスすると
LazyInitializationException
が発生します。
OSIVのメリット
-
遅延読み込みのサポート: 遅延ロードされたエンティティに ビューやコントローラー から自由にアクセスできます。OSIV が有効な状態では、サービス層でトランザクションが終了した後でも、ビューのレンダリング中にエンティティの遅延ロードフィールドをデータベースからロードできます。
-
開発の利便性: 永続コンテキストがビューのレンダリングまで維持されるため、コントローラーやビューからエンティティ間の関係を簡単に操作できます。
OSIVのデメリット
-
データベース接続の長期占有: OSIV が有効な場合、リクエストが完了するまでデータベースセッションを維持するため、データベース接続を長時間占有 する可能性があります。特に 同時リクエスト が多い環境では、パフォーマンス低下を引き起こす可能性があります。
-
パフォーマンスの低下: ビューで無計画に 遅延読み込み を使用すると、必要のない時点で多数のデータベースクエリが発生する可能性があります。これにより、N+1問題 などのパフォーマンス上の問題が生じることがあります。
-
トランザクション境界が不明確: 通常、トランザクションは サービス層で終了する べきですが、OSIV が有効な場合、ビューでもデータベースにアクセスできるため、トランザクションの境界 が不明確になる可能性があります。
OSIVの無効化
OSIV の無効化 は、パフォーマンスやデータベースリソース管理の観点から、高パフォーマンスのシステム でよく使用されます。OSIV を無効にすると、永続コンテキストは トランザクションが終了すると直ちに閉じられ、ビュー層ではデータベースにアクセスできなくなります。そのため、遅延読み込みされたフィールドにアクセスするには、サービス層で事前にデータをロードしておく必要があります。
OSIV の無効化方法 (Spring Boot)
application.properties
または application.yml
で次のように設定して、OSIV を無効にできます。
spring.jpa.open-in-view=false
OSIV の無効化時に発生する問題
-
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) を使用
サービス層で JPQL の JOIN 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]
結論
- OSIV は遅延読み込みをサポートし、開発の利便性を高めますが、パフォーマンスの低下 や データベース接続の長期占有 などのデメリットがあります。
-
OSIV を無効にする と、
LazyInitializationException
を回避するために、サービス層で フェッチ結合 や@EntityGraph
を使用して必要なデータを事前にロードするか、DTO パターン を使用することが一般的な解決策です。 - 高パフォーマンスのシステムでは OSIV の無効化 が望ましく、データロード戦略を明確に定義して管理することが重要です。