Talk about OpenSessionInView of spring data jpa.

  jpa

Order

This article mainly studies the OpenSessionInView of spring data jpa.

Open Session In View

  • Open session In View, OSIV for short, is to solve the LazyInitializationException exception exception thrown by no session when hibernate’s lazy load attribute is used in mvc controller. For hibernate, the ToMany relationship defaults to delayed loading, while the ToOne relationship defaults to immediate loading.

JpaProperties

spring-boot-autoconfigure-2.1.4.RELEASE-sources.jar! /org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java

@ConfigurationProperties(prefix = "spring.jpa")
public class JpaProperties {

    /**
     * Additional native properties to set on the JPA provider.
     */
    private Map<String, String> properties = new HashMap<>();

    /**
     * Mapping resources (equivalent to "mapping-file" entries in persistence.xml).
     */
    private final List<String> mappingResources = new ArrayList<>();

    /**
     * Name of the target database to operate on, auto-detected by default. Can be
     * alternatively set using the "Database" enum.
     */
    private String databasePlatform;

    /**
     * Target database to operate on, auto-detected by default. Can be alternatively set
     * using the "databasePlatform" property.
     */
    private Database database;

    /**
     * Whether to initialize the schema on startup.
     */
    private boolean generateDdl = false;

    /**
     * Whether to enable logging of SQL statements.
     */
    private boolean showSql = false;

    /**
     * Register OpenEntityManagerInViewInterceptor. Binds a JPA EntityManager to the
     * thread for the entire processing of the request.
     */
    private Boolean openInView;

    //......
}
  • JpaProperties has a configuration item called openInView (The default is true), used to decide whether to register OpenEntityManagerInviewInterceptor, which binds a JPA EntityManager to a requesting thread.

JpaBaseConfiguration

spring-boot-autoconfigure-2.1.4.RELEASE-sources.jar! /org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java

@Configuration
@EnableConfigurationProperties(JpaProperties.class)
@Import(DataSourceInitializedPublisher.Registrar.class)
public abstract class JpaBaseConfiguration implements BeanFactoryAware {
    //......

    @Configuration
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass(WebMvcConfigurer.class)
    @ConditionalOnMissingBean({ OpenEntityManagerInViewInterceptor.class,
            OpenEntityManagerInViewFilter.class })
    @ConditionalOnMissingFilterBean(OpenEntityManagerInViewFilter.class)
    @ConditionalOnProperty(prefix = "spring.jpa", name = "open-in-view",
            havingValue = "true", matchIfMissing = true)
    protected static class JpaWebConfiguration {

        // Defined as a nested config to ensure WebMvcConfigurerAdapter is not read when
        // not on the classpath
        @Configuration
        protected static class JpaWebMvcConfiguration implements WebMvcConfigurer {

            private static final Log logger = LogFactory
                    .getLog(JpaWebMvcConfiguration.class);

            private final JpaProperties jpaProperties;

            protected JpaWebMvcConfiguration(JpaProperties jpaProperties) {
                this.jpaProperties = jpaProperties;
            }

            @Bean
            public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
                if (this.jpaProperties.getOpenInView() == null) {
                    logger.warn("spring.jpa.open-in-view is enabled by default. "
                            + "Therefore, database queries may be performed during view "
                            + "rendering. Explicitly configure "
                            + "spring.jpa.open-in-view to disable this warning");
                }
                return new OpenEntityManagerInViewInterceptor();
            }

            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addWebRequestInterceptor(openEntityManagerInViewInterceptor());
            }

        }

    }

    //......
}
  • There is a JpaWebMvcConfiguration configuration in JpaBaseConfiguration. When the type of web application is Type.SERVLET and spring.jpa.open-in-view i s not false, register OpenEntityManagerInvisible Interceptor and add it to mvc’s webRequestInterceptor.

OpenEntityManagerInViewInterceptor

spring-orm-5.1.6.RELEASE-sources.jar! /org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java

public class OpenEntityManagerInViewInterceptor extends EntityManagerFactoryAccessor implements AsyncWebRequestInterceptor {

    /**
     * Suffix that gets appended to the EntityManagerFactory toString
     * representation for the "participate in existing entity manager
     * handling" request attribute.
     * @see #getParticipateAttributeName
     */
    public static final String PARTICIPATE_SUFFIX = ".PARTICIPATE";


    @Override
    public void preHandle(WebRequest request) throws DataAccessException {
        String key = getParticipateAttributeName();
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        if (asyncManager.hasConcurrentResult() && applyEntityManagerBindingInterceptor(asyncManager, key)) {
            return;
        }

        EntityManagerFactory emf = obtainEntityManagerFactory();
        if (TransactionSynchronizationManager.hasResource(emf)) {
            // Do not modify the EntityManager: just mark the request accordingly.
            Integer count = (Integer) request.getAttribute(key, WebRequest.SCOPE_REQUEST);
            int newCount = (count != null ? count + 1 : 1);
            request.setAttribute(getParticipateAttributeName(), newCount, WebRequest.SCOPE_REQUEST);
        }
        else {
            logger.debug("Opening JPA EntityManager in OpenEntityManagerInViewInterceptor");
            try {
                EntityManager em = createEntityManager();
                EntityManagerHolder emHolder = new EntityManagerHolder(em);
                TransactionSynchronizationManager.bindResource(emf, emHolder);

                AsyncRequestInterceptor interceptor = new AsyncRequestInterceptor(emf, emHolder);
                asyncManager.registerCallableInterceptor(key, interceptor);
                asyncManager.registerDeferredResultInterceptor(key, interceptor);
            }
            catch (PersistenceException ex) {
                throw new DataAccessResourceFailureException("Could not create JPA EntityManager", ex);
            }
        }
    }

    @Override
    public void postHandle(WebRequest request, @Nullable ModelMap model) {
    }

    @Override
    public void afterCompletion(WebRequest request, @Nullable Exception ex) throws DataAccessException {
        if (!decrementParticipateCount(request)) {
            EntityManagerHolder emHolder = (EntityManagerHolder)
                    TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory());
            logger.debug("Closing JPA EntityManager in OpenEntityManagerInViewInterceptor");
            EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager());
        }
    }

    private boolean decrementParticipateCount(WebRequest request) {
        String participateAttributeName = getParticipateAttributeName();
        Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST);
        if (count == null) {
            return false;
        }
        // Do not modify the Session: just clear the marker.
        if (count > 1) {
            request.setAttribute(participateAttributeName, count - 1, WebRequest.SCOPE_REQUEST);
        }
        else {
            request.removeAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST);
        }
        return true;
    }

    @Override
    public void afterConcurrentHandlingStarted(WebRequest request) {
        if (!decrementParticipateCount(request)) {
            TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory());
        }
    }

    /**
     * Return the name of the request attribute that identifies that a request is
     * already filtered. Default implementation takes the toString representation
     * of the EntityManagerFactory instance and appends ".FILTERED".
     * @see #PARTICIPATE_SUFFIX
     */
    protected String getParticipateAttributeName() {
        return obtainEntityManagerFactory().toString() + PARTICIPATE_SUFFIX;
    }


    private boolean applyEntityManagerBindingInterceptor(WebAsyncManager asyncManager, String key) {
        CallableProcessingInterceptor cpi = asyncManager.getCallableInterceptor(key);
        if (cpi == null) {
            return false;
        }
        ((AsyncRequestInterceptor) cpi).bindEntityManager();
        return true;
    }

}
  • OpenEntityManagerInviewInterceptor inherits the abstract class EntityManagerFactoryAccessor and implements the AsyncWebRequestInterceptor interface (The afterConcurrentHandlingStarted method is defined); AsyncWebRequestInterceptor inherits WebRequestInterceptor (The preHandle, postHandle, afterCompletion methods are defined.)
  • The preHandle method determines whether the current thread has an EntityManagerFactory, and if so, maintains count; in the attribute of the request; If not, EntityManager (openSession), and then bind using transactionsynchronizationmanager.binderresource.
  • The afterCompletion method decrements count in requestittribute (If there is one), remove the attribute; when count is 0; If request does not have count, use Transactional SynchronizationManager. Unbind Resource to unbind and then close EntityManager;; Asynchronous after erconcurrenthandlingstarted method is similar, mainly unbind operation

Summary

  • For hibernate, the ToMany relation defaults to delayed loading, while the ToOne relation defaults to immediate loading. However, in mvc’s controller, it is separated from the persisent contenxt XT, so entity becomes a detached state. at this time, the LazyInitializationException exception exception will be thrown when the delayed loading attribute is to be used, while the Open Session In View refers to solving this problem
  • There is a JpaWebMvcConfiguration configuration in JpaBaseConfiguration. When the type of web application is Type.SERVLET and spring.jpa.open-in-view i s not false, register OpenEntityManagerInvisible Interceptor and add it to mvc’s webRequestInterceptor.
  • The preHandle method of openEntityManagerinviewinterceptor determines whether the current thread has EntityManagerFactory, and if not, creates entitymanager (openSessionBindresource) to the current thread; The afterCompletion method uses Transactional SynchronizationManager. unbindResource to unbind and then close EntityManager.

Solving the LazyInitialization problem through OSIV technology will lead to the long life cycle of open’s session, which runs through the entire request. The session can be closed to release the database connection only after the view is rendered. In addition, OSIV exposes the technical details of the service layer to the controller layer, causing certain coupling, so it is not recommended to open it. the corresponding solution is to use dto in the controller layer instead of the entity of the detached state. the required data is no longer dependent on delayed loading, and it is explicitly queried as needed when assembling dto

doc