diff --git a/build.gradle b/build.gradle index 6058893b55..d6659037ac 100644 --- a/build.gradle +++ b/build.gradle @@ -827,6 +827,21 @@ project("spring-orm-hibernate4") { } } +project("spring-orm-hibernate5") { + description = "Spring Object/Relational Mapping - Hibernate 5 support" + merge.into = project(":spring-orm") + + dependencies { + provided(project(":spring-jdbc")) + provided(project(":spring-tx")) + optional(project(":spring-web")) + optional("org.hibernate:hibernate-core:5.0.0.Beta2") + optional("org.hibernate:hibernate-entitymanager:5.0.0.Beta2") + optional("javax.servlet:javax.servlet-api:3.0.1") + optional("aopalliance:aopalliance:1.0") + } +} + project("spring-webmvc") { description = "Spring Web MVC" diff --git a/settings.gradle b/settings.gradle index 8ee1787cd4..aa0763c488 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,6 +15,7 @@ include "spring-jms" include "spring-messaging" include "spring-orm" include "spring-orm-hibernate4" +include "spring-orm-hibernate5" include "spring-oxm" include "spring-test" include "spring-tx" diff --git a/spring-orm-hibernate4/src/main/java/org/springframework/orm/jpa/vendor/SpringHibernateJpaPersistenceProvider.java b/spring-orm-hibernate4/src/main/java/org/springframework/orm/jpa/vendor/SpringHibernateJpaPersistenceProvider.java index a3e147cde5..88e063bfbe 100644 --- a/spring-orm-hibernate4/src/main/java/org/springframework/orm/jpa/vendor/SpringHibernateJpaPersistenceProvider.java +++ b/spring-orm-hibernate4/src/main/java/org/springframework/orm/jpa/vendor/SpringHibernateJpaPersistenceProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.orm.jpa.vendor; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import javax.persistence.EntityManagerFactory; import javax.persistence.spi.PersistenceUnitInfo; @@ -24,7 +26,6 @@ import org.hibernate.cfg.Configuration; import org.hibernate.jpa.HibernatePersistenceProvider; import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor; -import org.hibernate.service.ServiceRegistry; import org.springframework.orm.jpa.persistenceunit.SmartPersistenceUnitInfo; @@ -33,7 +34,7 @@ import org.springframework.orm.jpa.persistenceunit.SmartPersistenceUnitInfo; * from the {@code org.hibernate.jpa} package, adding support for * {@link SmartPersistenceUnitInfo#getManagedPackages()}. * - *

Compatible with Hibernate 4.3. {@link SpringHibernateEjbPersistenceProvider} + *

Compatible with Hibernate 4.3-5.0. {@link SpringHibernateEjbPersistenceProvider} * is an alternative for compatibility with earlier Hibernate versions (3.6-4.2). * * @author Juergen Hoeller @@ -45,19 +46,18 @@ class SpringHibernateJpaPersistenceProvider extends HibernatePersistenceProvider @Override @SuppressWarnings("rawtypes") - public EntityManagerFactory createContainerEntityManagerFactory(final PersistenceUnitInfo info, Map properties) { - return new EntityManagerFactoryBuilderImpl(new PersistenceUnitInfoDescriptor(info), properties) { - @Override - public Configuration buildHibernateConfiguration(ServiceRegistry serviceRegistry) { - Configuration configuration = super.buildHibernateConfiguration(serviceRegistry); - if (info instanceof SmartPersistenceUnitInfo) { - for (String managedPackage : ((SmartPersistenceUnitInfo) info).getManagedPackages()) { - configuration.addPackage(managedPackage); + public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map properties) { + final List mergedClassesAndPackages = new ArrayList(info.getManagedClassNames()); + if (info instanceof SmartPersistenceUnitInfo) { + mergedClassesAndPackages.addAll(((SmartPersistenceUnitInfo) info).getManagedPackages()); + } + return new EntityManagerFactoryBuilderImpl( + new PersistenceUnitInfoDescriptor(info) { + @Override + public List getManagedClassNames() { + return mergedClassesAndPackages; } - } - return configuration; - } - }.build(); + }, properties).build(); } } diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/ConfigurableJtaPlatform.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/ConfigurableJtaPlatform.java new file mode 100644 index 0000000000..e1c74b58ef --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/ConfigurableJtaPlatform.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import javax.transaction.Status; +import javax.transaction.Synchronization; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.TransactionSynchronizationRegistry; +import javax.transaction.UserTransaction; + +import org.hibernate.TransactionException; +import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; + +import org.springframework.transaction.jta.UserTransactionAdapter; +import org.springframework.util.Assert; + +/** + * Implementation of Hibernate 4's JtaPlatform SPI, exposing passed-in {@link TransactionManager}, + * {@link UserTransaction} and {@link TransactionSynchronizationRegistry} references. + * + * @author Juergen Hoeller + * @since 4.2 + */ +@SuppressWarnings("serial") +class ConfigurableJtaPlatform implements JtaPlatform { + + private final TransactionManager transactionManager; + + private final UserTransaction userTransaction; + + private final TransactionSynchronizationRegistry transactionSynchronizationRegistry; + + + /** + * Create a new ConfigurableJtaPlatform instance with the given + * JTA TransactionManager and optionally a given UserTransaction. + * @param tm the JTA TransactionManager reference (required) + * @param ut the JTA UserTransaction reference (optional) + * @param tsr the JTA 1.1 TransactionSynchronizationRegistry (optional) + */ + public ConfigurableJtaPlatform(TransactionManager tm, UserTransaction ut, TransactionSynchronizationRegistry tsr) { + Assert.notNull(tm, "TransactionManager reference must not be null"); + this.transactionManager = tm; + this.userTransaction = (ut != null ? ut : new UserTransactionAdapter(tm)); + this.transactionSynchronizationRegistry = tsr; + } + + + @Override + public TransactionManager retrieveTransactionManager() { + return this.transactionManager; + } + + @Override + public UserTransaction retrieveUserTransaction() { + return this.userTransaction; + } + + @Override + public Object getTransactionIdentifier(Transaction transaction) { + return transaction; + } + + @Override + public boolean canRegisterSynchronization() { + try { + return (this.transactionManager.getStatus() == Status.STATUS_ACTIVE); + } + catch (SystemException ex) { + throw new TransactionException("Could not determine JTA transaction status", ex); + } + } + + @Override + public void registerSynchronization(Synchronization synchronization) { + if (this.transactionSynchronizationRegistry != null) { + this.transactionSynchronizationRegistry.registerInterposedSynchronization(synchronization); + } + else { + try { + this.transactionManager.getTransaction().registerSynchronization(synchronization); + } + catch (Exception ex) { + throw new TransactionException("Could not access JTA Transaction to register synchronization", ex); + } + } + } + + @Override + public int getCurrentStatus() throws SystemException { + return this.transactionManager.getStatus(); + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateCallback.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateCallback.java new file mode 100644 index 0000000000..78074360ca --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateCallback.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import org.hibernate.HibernateException; +import org.hibernate.Session; + +/** + * Callback interface for Hibernate code. To be used with {@link HibernateTemplate}'s + * execution methods, often as anonymous classes within a method implementation. + * A typical implementation will call {@code Session.load/find/update} to perform + * some operations on persistent objects. + * + * @author Juergen Hoeller + * @since 4.2 + * @see HibernateTemplate + * @see HibernateTransactionManager + */ +public interface HibernateCallback { + + /** + * Gets called by {@code HibernateTemplate.execute} with an active + * Hibernate {@code Session}. Does not need to care about activating + * or closing the {@code Session}, or handling transactions. + * + *

Allows for returning a result object created within the callback, + * i.e. a domain object or a collection of domain objects. + * A thrown custom RuntimeException is treated as an application exception: + * It gets propagated to the caller of the template. + * + * @param session active Hibernate session + * @return a result object, or {@code null} if none + * @throws HibernateException if thrown by the Hibernate API + * @see HibernateTemplate#execute + */ + T doInHibernate(Session session) throws HibernateException; + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateExceptionTranslator.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateExceptionTranslator.java new file mode 100644 index 0000000000..5e4886f2f8 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateExceptionTranslator.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import org.hibernate.HibernateException; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.PersistenceExceptionTranslator; + +/** + * {@link PersistenceExceptionTranslator} capable of translating {@link HibernateException} + * instances to Spring's {@link DataAccessException} hierarchy. + * + *

Extended by {@link LocalSessionFactoryBean}, so there is no need to declare this + * translator in addition to a {@code LocalSessionFactoryBean}. + * + *

When configuring the container with {@code @Configuration} classes, a {@code @Bean} + * of this type must be registered manually. + * + * @author Juergen Hoeller + * @since 4.2 + * @see org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor + * @see SessionFactoryUtils#convertHibernateAccessException(HibernateException) + */ +public class HibernateExceptionTranslator implements PersistenceExceptionTranslator { + + @Override + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + if (ex instanceof HibernateException) { + return convertHibernateAccessException((HibernateException) ex); + } + return null; + } + + /** + * Convert the given HibernateException to an appropriate exception from the + * {@code org.springframework.dao} hierarchy. + * @param ex HibernateException that occured + * @return a corresponding DataAccessException + * @see SessionFactoryUtils#convertHibernateAccessException + */ + protected DataAccessException convertHibernateAccessException(HibernateException ex) { + return SessionFactoryUtils.convertHibernateAccessException(ex); + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateJdbcException.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateJdbcException.java new file mode 100644 index 0000000000..4d0bb9d593 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateJdbcException.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import java.sql.SQLException; + +import org.hibernate.JDBCException; + +import org.springframework.dao.UncategorizedDataAccessException; + +/** + * Hibernate-specific subclass of UncategorizedDataAccessException, + * for JDBC exceptions that Hibernate wrapped. + * + * @author Juergen Hoeller + * @since 4.2 + * @see SessionFactoryUtils#convertHibernateAccessException + */ +@SuppressWarnings("serial") +public class HibernateJdbcException extends UncategorizedDataAccessException { + + public HibernateJdbcException(JDBCException ex) { + super("JDBC exception on Hibernate data access: SQLException for SQL [" + ex.getSQL() + "]; SQL state [" + + ex.getSQLState() + "]; error code [" + ex.getErrorCode() + "]; " + ex.getMessage(), ex); + } + + /** + * Return the underlying SQLException. + */ + public SQLException getSQLException() { + return ((JDBCException) getCause()).getSQLException(); + } + + /** + * Return the SQL that led to the problem. + */ + public String getSql() { + return ((JDBCException) getCause()).getSQL(); + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateObjectRetrievalFailureException.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateObjectRetrievalFailureException.java new file mode 100644 index 0000000000..f4c96d5f69 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateObjectRetrievalFailureException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import org.hibernate.UnresolvableObjectException; +import org.hibernate.WrongClassException; + +import org.springframework.orm.ObjectRetrievalFailureException; + +/** + * Hibernate-specific subclass of ObjectRetrievalFailureException. + * Converts Hibernate's UnresolvableObjectException and WrongClassException. + * + * @author Juergen Hoeller + * @since 4.2 + * @see SessionFactoryUtils#convertHibernateAccessException + */ +@SuppressWarnings("serial") +public class HibernateObjectRetrievalFailureException extends ObjectRetrievalFailureException { + + public HibernateObjectRetrievalFailureException(UnresolvableObjectException ex) { + super(ex.getEntityName(), ex.getIdentifier(), ex.getMessage(), ex); + } + + public HibernateObjectRetrievalFailureException(WrongClassException ex) { + super(ex.getEntityName(), ex.getIdentifier(), ex.getMessage(), ex); + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateOperations.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateOperations.java new file mode 100644 index 0000000000..d3ae8d7f0b --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateOperations.java @@ -0,0 +1,795 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.hibernate.Filter; +import org.hibernate.LockMode; +import org.hibernate.ReplicationMode; +import org.hibernate.criterion.DetachedCriteria; + +import org.springframework.dao.DataAccessException; + +/** + * Interface that specifies a basic set of Hibernate operations, + * implemented by {@link HibernateTemplate}. Not often used, but a useful + * option to enhance testability, as it can easily be mocked or stubbed. + * + *

Defines {@code HibernateTemplate}'s data access methods that + * mirror various {@link org.hibernate.Session} methods. Users are + * strongly encouraged to read the Hibernate {@code Session} javadocs + * for details on the semantics of those methods. + * + * @author Juergen Hoeller + * @since 4.2 + * @see HibernateTemplate + * @see org.hibernate.Session + * @see HibernateTransactionManager + */ +public interface HibernateOperations { + + /** + * Execute the action specified by the given action object within a + * {@link org.hibernate.Session}. + *

Application exceptions thrown by the action object get propagated + * to the caller (can only be unchecked). Hibernate exceptions are + * transformed into appropriate DAO ones. Allows for returning a result + * object, that is a domain object or a collection of domain objects. + *

Note: Callback code is not supposed to handle transactions itself! + * Use an appropriate transaction manager like + * {@link HibernateTransactionManager}. Generally, callback code must not + * touch any {@code Session} lifecycle methods, like close, + * disconnect, or reconnect, to let the template do its work. + * @param action callback object that specifies the Hibernate action + * @return a result object returned by the action, or {@code null} + * @throws DataAccessException in case of Hibernate errors + * @see HibernateTransactionManager + * @see org.hibernate.Session + */ + T execute(HibernateCallback action) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience methods for loading individual objects + //------------------------------------------------------------------------- + + /** + * Return the persistent instance of the given entity class + * with the given identifier, or {@code null} if not found. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#get(Class, Serializable)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityClass a persistent class + * @param id the identifier of the persistent instance + * @return the persistent instance, or {@code null} if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#get(Class, Serializable) + */ + T get(Class entityClass, Serializable id) throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, or {@code null} if not found. + *

Obtains the specified lock mode if the instance exists. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#get(Class, Serializable, LockMode)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityClass a persistent class + * @param id the identifier of the persistent instance + * @param lockMode the lock mode to obtain + * @return the persistent instance, or {@code null} if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#get(Class, Serializable, LockMode) + */ + T get(Class entityClass, Serializable id, LockMode lockMode) throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, or {@code null} if not found. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#get(String, Serializable)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityName the name of the persistent entity + * @param id the identifier of the persistent instance + * @return the persistent instance, or {@code null} if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#get(Class, Serializable) + */ + Object get(String entityName, Serializable id) throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, or {@code null} if not found. + * Obtains the specified lock mode if the instance exists. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#get(String, Serializable, LockMode)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityName the name of the persistent entity + * @param id the identifier of the persistent instance + * @param lockMode the lock mode to obtain + * @return the persistent instance, or {@code null} if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#get(Class, Serializable, LockMode) + */ + Object get(String entityName, Serializable id, LockMode lockMode) throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, throwing an exception if not found. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#load(Class, Serializable)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityClass a persistent class + * @param id the identifier of the persistent instance + * @return the persistent instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#load(Class, Serializable) + */ + T load(Class entityClass, Serializable id) throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, throwing an exception if not found. + * Obtains the specified lock mode if the instance exists. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#load(Class, Serializable, LockMode)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityClass a persistent class + * @param id the identifier of the persistent instance + * @param lockMode the lock mode to obtain + * @return the persistent instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#load(Class, Serializable) + */ + T load(Class entityClass, Serializable id, LockMode lockMode) throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, throwing an exception if not found. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#load(String, Serializable)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityName the name of the persistent entity + * @param id the identifier of the persistent instance + * @return the persistent instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#load(Class, Serializable) + */ + Object load(String entityName, Serializable id) throws DataAccessException; + + /** + * Return the persistent instance of the given entity class + * with the given identifier, throwing an exception if not found. + *

Obtains the specified lock mode if the instance exists. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#load(String, Serializable, LockMode)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entityName the name of the persistent entity + * @param id the identifier of the persistent instance + * @param lockMode the lock mode to obtain + * @return the persistent instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#load(Class, Serializable) + */ + Object load(String entityName, Serializable id, LockMode lockMode) throws DataAccessException; + + /** + * Return all persistent instances of the given entity class. + * Note: Use queries or criteria for retrieving a specific subset. + * @param entityClass a persistent class + * @return a {@link List} containing 0 or more persistent instances + * @throws DataAccessException if there is a Hibernate error + * @see org.hibernate.Session#createCriteria + */ + List loadAll(Class entityClass) throws DataAccessException; + + /** + * Load the persistent instance with the given identifier + * into the given object, throwing an exception if not found. + *

This method is a thin wrapper around + * {@link org.hibernate.Session#load(Object, Serializable)} for convenience. + * For an explanation of the exact semantics of this method, please do refer to + * the Hibernate API documentation in the first instance. + * @param entity the object (of the target class) to load into + * @param id the identifier of the persistent instance + * @throws org.springframework.orm.ObjectRetrievalFailureException if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#load(Object, Serializable) + */ + void load(Object entity, Serializable id) throws DataAccessException; + + /** + * Re-read the state of the given persistent instance. + * @param entity the persistent instance to re-read + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#refresh(Object) + */ + void refresh(Object entity) throws DataAccessException; + + /** + * Re-read the state of the given persistent instance. + * Obtains the specified lock mode for the instance. + * @param entity the persistent instance to re-read + * @param lockMode the lock mode to obtain + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#refresh(Object, LockMode) + */ + void refresh(Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Check whether the given object is in the Session cache. + * @param entity the persistence instance to check + * @return whether the given object is in the Session cache + * @throws DataAccessException if there is a Hibernate error + * @see org.hibernate.Session#contains + */ + boolean contains(Object entity) throws DataAccessException; + + /** + * Remove the given object from the {@link org.hibernate.Session} cache. + * @param entity the persistent instance to evict + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#evict + */ + void evict(Object entity) throws DataAccessException; + + /** + * Force initialization of a Hibernate proxy or persistent collection. + * @param proxy a proxy for a persistent object or a persistent collection + * @throws DataAccessException if we can't initialize the proxy, for example + * because it is not associated with an active Session + * @see org.hibernate.Hibernate#initialize + */ + void initialize(Object proxy) throws DataAccessException; + + /** + * Return an enabled Hibernate {@link Filter} for the given filter name. + * The returned {@code Filter} instance can be used to set filter parameters. + * @param filterName the name of the filter + * @return the enabled Hibernate {@code Filter} (either already + * enabled or enabled on the fly by this operation) + * @throws IllegalStateException if we are not running within a + * transactional Session (in which case this operation does not make sense) + */ + Filter enableFilter(String filterName) throws IllegalStateException; + + + //------------------------------------------------------------------------- + // Convenience methods for storing individual objects + //------------------------------------------------------------------------- + + /** + * Obtain the specified lock level upon the given object, implicitly + * checking whether the corresponding database entry still exists. + * @param entity the persistent instance to lock + * @param lockMode the lock mode to obtain + * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#lock(Object, LockMode) + */ + void lock(Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Obtain the specified lock level upon the given object, implicitly + * checking whether the corresponding database entry still exists. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to lock + * @param lockMode the lock mode to obtain + * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#lock(String, Object, LockMode) + */ + void lock(String entityName, Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Persist the given transient instance. + * @param entity the transient instance to persist + * @return the generated identifier + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#save(Object) + */ + Serializable save(Object entity) throws DataAccessException; + + /** + * Persist the given transient instance. + * @param entityName the name of the persistent entity + * @param entity the transient instance to persist + * @return the generated identifier + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#save(String, Object) + */ + Serializable save(String entityName, Object entity) throws DataAccessException; + + /** + * Update the given persistent instance, + * associating it with the current Hibernate {@link org.hibernate.Session}. + * @param entity the persistent instance to update + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#update(Object) + */ + void update(Object entity) throws DataAccessException; + + /** + * Update the given persistent instance, + * associating it with the current Hibernate {@link org.hibernate.Session}. + *

Obtains the specified lock mode if the instance exists, implicitly + * checking whether the corresponding database entry still exists. + * @param entity the persistent instance to update + * @param lockMode the lock mode to obtain + * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#update(Object) + */ + void update(Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Update the given persistent instance, + * associating it with the current Hibernate {@link org.hibernate.Session}. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to update + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#update(String, Object) + */ + void update(String entityName, Object entity) throws DataAccessException; + + /** + * Update the given persistent instance, + * associating it with the current Hibernate {@link org.hibernate.Session}. + *

Obtains the specified lock mode if the instance exists, implicitly + * checking whether the corresponding database entry still exists. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to update + * @param lockMode the lock mode to obtain + * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#update(String, Object) + */ + void update(String entityName, Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Save or update the given persistent instance, + * according to its id (matching the configured "unsaved-value"?). + * Associates the instance with the current Hibernate {@link org.hibernate.Session}. + * @param entity the persistent instance to save or update + * (to be associated with the Hibernate {@code Session}) + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#saveOrUpdate(Object) + */ + void saveOrUpdate(Object entity) throws DataAccessException; + + /** + * Save or update the given persistent instance, + * according to its id (matching the configured "unsaved-value"?). + * Associates the instance with the current Hibernate {@code Session}. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to save or update + * (to be associated with the Hibernate {@code Session}) + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#saveOrUpdate(String, Object) + */ + void saveOrUpdate(String entityName, Object entity) throws DataAccessException; + + /** + * Persist the state of the given detached instance according to the + * given replication mode, reusing the current identifier value. + * @param entity the persistent object to replicate + * @param replicationMode the Hibernate ReplicationMode + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#replicate(Object, ReplicationMode) + */ + void replicate(Object entity, ReplicationMode replicationMode) throws DataAccessException; + + /** + * Persist the state of the given detached instance according to the + * given replication mode, reusing the current identifier value. + * @param entityName the name of the persistent entity + * @param entity the persistent object to replicate + * @param replicationMode the Hibernate ReplicationMode + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#replicate(String, Object, ReplicationMode) + */ + void replicate(String entityName, Object entity, ReplicationMode replicationMode) throws DataAccessException; + + /** + * Persist the given transient instance. Follows JSR-220 semantics. + *

Similar to {@code save}, associating the given object + * with the current Hibernate {@link org.hibernate.Session}. + * @param entity the persistent instance to persist + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#persist(Object) + * @see #save + */ + void persist(Object entity) throws DataAccessException; + + /** + * Persist the given transient instance. Follows JSR-220 semantics. + *

Similar to {@code save}, associating the given object + * with the current Hibernate {@link org.hibernate.Session}. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to persist + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#persist(String, Object) + * @see #save + */ + void persist(String entityName, Object entity) throws DataAccessException; + + /** + * Copy the state of the given object onto the persistent object + * with the same identifier. Follows JSR-220 semantics. + *

Similar to {@code saveOrUpdate}, but never associates the given + * object with the current Hibernate Session. In case of a new entity, + * the state will be copied over as well. + *

Note that {@code merge} will not update the identifiers + * in the passed-in object graph (in contrast to TopLink)! Consider + * registering Spring's {@code IdTransferringMergeEventListener} if + * you would like to have newly assigned ids transferred to the original + * object graph too. + * @param entity the object to merge with the corresponding persistence instance + * @return the updated, registered persistent instance + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#merge(Object) + * @see #saveOrUpdate + */ + T merge(T entity) throws DataAccessException; + + /** + * Copy the state of the given object onto the persistent object + * with the same identifier. Follows JSR-220 semantics. + *

Similar to {@code saveOrUpdate}, but never associates the given + * object with the current Hibernate {@link org.hibernate.Session}. In + * the case of a new entity, the state will be copied over as well. + *

Note that {@code merge} will not update the identifiers + * in the passed-in object graph (in contrast to TopLink)! Consider + * registering Spring's {@code IdTransferringMergeEventListener} + * if you would like to have newly assigned ids transferred to the + * original object graph too. + * @param entityName the name of the persistent entity + * @param entity the object to merge with the corresponding persistence instance + * @return the updated, registered persistent instance + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#merge(String, Object) + * @see #saveOrUpdate + */ + T merge(String entityName, T entity) throws DataAccessException; + + /** + * Delete the given persistent instance. + * @param entity the persistent instance to delete + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#delete(Object) + */ + void delete(Object entity) throws DataAccessException; + + /** + * Delete the given persistent instance. + *

Obtains the specified lock mode if the instance exists, implicitly + * checking whether the corresponding database entry still exists. + * @param entity the persistent instance to delete + * @param lockMode the lock mode to obtain + * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#delete(Object) + */ + void delete(Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Delete the given persistent instance. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to delete + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#delete(Object) + */ + void delete(String entityName, Object entity) throws DataAccessException; + + /** + * Delete the given persistent instance. + *

Obtains the specified lock mode if the instance exists, implicitly + * checking whether the corresponding database entry still exists. + * @param entityName the name of the persistent entity + * @param entity the persistent instance to delete + * @param lockMode the lock mode to obtain + * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#delete(Object) + */ + void delete(String entityName, Object entity, LockMode lockMode) throws DataAccessException; + + /** + * Delete all given persistent instances. + *

This can be combined with any of the find methods to delete by query + * in two lines of code. + * @param entities the persistent instances to delete + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#delete(Object) + */ + void deleteAll(Collection entities) throws DataAccessException; + + /** + * Flush all pending saves, updates and deletes to the database. + *

Only invoke this for selective eager flushing, for example when + * JDBC code needs to see certain changes within the same transaction. + * Else, it is preferable to rely on auto-flushing at transaction + * completion. + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#flush + */ + void flush() throws DataAccessException; + + /** + * Remove all objects from the {@link org.hibernate.Session} cache, and + * cancel all pending saves, updates and deletes. + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#clear + */ + void clear() throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience finder methods for HQL strings + //------------------------------------------------------------------------- + + /** + * Execute an HQL query, binding a number of values to "?" parameters + * in the query string. + * @param queryString a query expressed in Hibernate's query language + * @param values the values of the parameters + * @return a {@link List} containing the results of the query execution + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#createQuery + */ + List find(String queryString, Object... values) throws DataAccessException; + + /** + * Execute an HQL query, binding one value to a ":" named parameter + * in the query string. + * @param queryString a query expressed in Hibernate's query language + * @param paramName the name of the parameter + * @param value the value of the parameter + * @return a {@link List} containing the results of the query execution + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedParam(String queryString, String paramName, Object value) throws DataAccessException; + + /** + * Execute an HQL query, binding a number of values to ":" named + * parameters in the query string. + * @param queryString a query expressed in Hibernate's query language + * @param paramNames the names of the parameters + * @param values the values of the parameters + * @return a {@link List} containing the results of the query execution + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedParam(String queryString, String[] paramNames, Object[] values) throws DataAccessException; + + /** + * Execute an HQL query, binding the properties of the given bean to + * named parameters in the query string. + * @param queryString a query expressed in Hibernate's query language + * @param valueBean the values of the parameters + * @return a {@link List} containing the results of the query execution + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Query#setProperties + * @see org.hibernate.Session#createQuery + */ + List findByValueBean(String queryString, Object valueBean) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience finder methods for named queries + //------------------------------------------------------------------------- + + /** + * Execute a named query binding a number of values to "?" parameters + * in the query string. + *

A named query is defined in a Hibernate mapping file. + * @param queryName the name of a Hibernate query in a mapping file + * @param values the values of the parameters + * @return a {@link List} containing the results of the query execution + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedQuery(String queryName, Object... values) throws DataAccessException; + + /** + * Execute a named query, binding one value to a ":" named parameter + * in the query string. + *

A named query is defined in a Hibernate mapping file. + * @param queryName the name of a Hibernate query in a mapping file + * @param paramName the name of parameter + * @param value the value of the parameter + * @return a {@link List} containing the results of the query execution + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedQueryAndNamedParam(String queryName, String paramName, Object value) + throws DataAccessException; + + /** + * Execute a named query, binding a number of values to ":" named + * parameters in the query string. + *

A named query is defined in a Hibernate mapping file. + * @param queryName the name of a Hibernate query in a mapping file + * @param paramNames the names of the parameters + * @param values the values of the parameters + * @return a {@link List} containing the results of the query execution + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedQueryAndNamedParam(String queryName, String[] paramNames, Object[] values) + throws DataAccessException; + + /** + * Execute a named query, binding the properties of the given bean to + * ":" named parameters in the query string. + *

A named query is defined in a Hibernate mapping file. + * @param queryName the name of a Hibernate query in a mapping file + * @param valueBean the values of the parameters + * @return a {@link List} containing the results of the query execution + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Query#setProperties + * @see org.hibernate.Session#getNamedQuery(String) + */ + List findByNamedQueryAndValueBean(String queryName, Object valueBean) throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience finder methods for detached criteria + //------------------------------------------------------------------------- + + /** + * Execute a query based on a given Hibernate criteria object. + * @param criteria the detached Hibernate criteria object. + * Note: Do not reuse criteria objects! They need to recreated per execution, + * due to the suboptimal design of Hibernate's criteria facility. + * @return a {@link List} containing 0 or more persistent instances + * @throws DataAccessException in case of Hibernate errors + * @see DetachedCriteria#getExecutableCriteria(org.hibernate.Session) + */ + List findByCriteria(DetachedCriteria criteria) throws DataAccessException; + + /** + * Execute a query based on the given Hibernate criteria object. + * @param criteria the detached Hibernate criteria object. + * Note: Do not reuse criteria objects! They need to recreated per execution, + * due to the suboptimal design of Hibernate's criteria facility. + * @param firstResult the index of the first result object to be retrieved + * (numbered from 0) + * @param maxResults the maximum number of result objects to retrieve + * (or <=0 for no limit) + * @return a {@link List} containing 0 or more persistent instances + * @throws DataAccessException in case of Hibernate errors + * @see DetachedCriteria#getExecutableCriteria(org.hibernate.Session) + * @see org.hibernate.Criteria#setFirstResult(int) + * @see org.hibernate.Criteria#setMaxResults(int) + */ + List findByCriteria(DetachedCriteria criteria, int firstResult, int maxResults) throws DataAccessException; + + /** + * Execute a query based on the given example entity object. + * @param exampleEntity an instance of the desired entity, + * serving as example for "query-by-example" + * @return a {@link List} containing 0 or more persistent instances + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.criterion.Example#create(Object) + */ + List findByExample(T exampleEntity) throws DataAccessException; + + /** + * Execute a query based on the given example entity object. + * @param entityName the name of the persistent entity + * @param exampleEntity an instance of the desired entity, + * serving as example for "query-by-example" + * @return a {@link List} containing 0 or more persistent instances + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.criterion.Example#create(Object) + */ + List findByExample(String entityName, T exampleEntity) throws DataAccessException; + + /** + * Execute a query based on a given example entity object. + * @param exampleEntity an instance of the desired entity, + * serving as example for "query-by-example" + * @param firstResult the index of the first result object to be retrieved + * (numbered from 0) + * @param maxResults the maximum number of result objects to retrieve + * (or <=0 for no limit) + * @return a {@link List} containing 0 or more persistent instances + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.criterion.Example#create(Object) + * @see org.hibernate.Criteria#setFirstResult(int) + * @see org.hibernate.Criteria#setMaxResults(int) + */ + List findByExample(T exampleEntity, int firstResult, int maxResults) throws DataAccessException; + + /** + * Execute a query based on a given example entity object. + * @param entityName the name of the persistent entity + * @param exampleEntity an instance of the desired entity, + * serving as example for "query-by-example" + * @param firstResult the index of the first result object to be retrieved + * (numbered from 0) + * @param maxResults the maximum number of result objects to retrieve + * (or <=0 for no limit) + * @return a {@link List} containing 0 or more persistent instances + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.criterion.Example#create(Object) + * @see org.hibernate.Criteria#setFirstResult(int) + * @see org.hibernate.Criteria#setMaxResults(int) + */ + List findByExample(String entityName, T exampleEntity, int firstResult, int maxResults) + throws DataAccessException; + + + //------------------------------------------------------------------------- + // Convenience query methods for iteration and bulk updates/deletes + //------------------------------------------------------------------------- + + /** + * Execute a query for persistent instances, binding a number of + * values to "?" parameters in the query string. + *

Returns the results as an {@link Iterator}. Entities returned are + * initialized on demand. See the Hibernate API documentation for details. + * @param queryString a query expressed in Hibernate's query language + * @param values the values of the parameters + * @return an {@link Iterator} containing 0 or more persistent instances + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#createQuery + * @see org.hibernate.Query#iterate + */ + Iterator iterate(String queryString, Object... values) throws DataAccessException; + + /** + * Immediately close an {@link Iterator} created by any of the various + * {@code iterate(..)} operations, instead of waiting until the + * session is closed or disconnected. + * @param it the {@code Iterator} to close + * @throws DataAccessException if the {@code Iterator} could not be closed + * @see org.hibernate.Hibernate#close + */ + void closeIterator(Iterator it) throws DataAccessException; + + /** + * Update/delete all objects according to the given query, binding a number of + * values to "?" parameters in the query string. + * @param queryString an update/delete query expressed in Hibernate's query language + * @param values the values of the parameters + * @return the number of instances updated/deleted + * @throws DataAccessException in case of Hibernate errors + * @see org.hibernate.Session#createQuery + * @see org.hibernate.Query#executeUpdate + */ + int bulkUpdate(String queryString, Object... values) throws DataAccessException; + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateOptimisticLockingFailureException.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateOptimisticLockingFailureException.java new file mode 100644 index 0000000000..0d9121f1a7 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateOptimisticLockingFailureException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import org.hibernate.StaleObjectStateException; +import org.hibernate.StaleStateException; +import org.hibernate.dialect.lock.OptimisticEntityLockException; + +import org.springframework.orm.ObjectOptimisticLockingFailureException; + +/** + * Hibernate-specific subclass of ObjectOptimisticLockingFailureException. + * Converts Hibernate's StaleObjectStateException, StaleStateException + * and OptimisticEntityLockException. + * + * @author Juergen Hoeller + * @since 4.2 + * @see SessionFactoryUtils#convertHibernateAccessException + */ +@SuppressWarnings("serial") +public class HibernateOptimisticLockingFailureException extends ObjectOptimisticLockingFailureException { + + public HibernateOptimisticLockingFailureException(StaleObjectStateException ex) { + super(ex.getEntityName(), ex.getIdentifier(), ex); + } + + public HibernateOptimisticLockingFailureException(StaleStateException ex) { + super(ex.getMessage(), ex); + } + + public HibernateOptimisticLockingFailureException(OptimisticEntityLockException ex) { + super(ex.getMessage(), ex); + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateQueryException.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateQueryException.java new file mode 100644 index 0000000000..013c362315 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateQueryException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import org.hibernate.QueryException; + +import org.springframework.dao.InvalidDataAccessResourceUsageException; + +/** + * Hibernate-specific subclass of InvalidDataAccessResourceUsageException, + * thrown on invalid HQL query syntax. + * + * @author Juergen Hoeller + * @since 4.2 + * @see SessionFactoryUtils#convertHibernateAccessException + */ +@SuppressWarnings("serial") +public class HibernateQueryException extends InvalidDataAccessResourceUsageException { + + public HibernateQueryException(QueryException ex) { + super(ex.getMessage(), ex); + } + + /** + * Return the HQL query string that was invalid. + */ + public String getQueryString() { + return ((QueryException) getCause()).getQueryString(); + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateSystemException.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateSystemException.java new file mode 100644 index 0000000000..21ef842abe --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateSystemException.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import org.hibernate.HibernateException; + +import org.springframework.dao.UncategorizedDataAccessException; + +/** + * Hibernate-specific subclass of UncategorizedDataAccessException, + * for Hibernate system errors that do not match any concrete + * {@code org.springframework.dao} exceptions. + * + * @author Juergen Hoeller + * @since 4.2 + * @see SessionFactoryUtils#convertHibernateAccessException + */ +@SuppressWarnings("serial") +public class HibernateSystemException extends UncategorizedDataAccessException { + + /** + * Create a new HibernateSystemException, + * wrapping an arbitrary HibernateException. + * @param cause the HibernateException thrown + */ + public HibernateSystemException(HibernateException cause) { + super(cause != null ? cause.getMessage() : null, cause); + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateTemplate.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateTemplate.java new file mode 100644 index 0000000000..f52d46c1fa --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateTemplate.java @@ -0,0 +1,1270 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.Criteria; +import org.hibernate.Filter; +import org.hibernate.FlushMode; +import org.hibernate.Hibernate; +import org.hibernate.HibernateException; +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.Query; +import org.hibernate.ReplicationMode; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.criterion.DetachedCriteria; +import org.hibernate.criterion.Example; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Helper class that simplifies Hibernate data access code. Automatically + * converts HibernateExceptions into DataAccessExceptions, following the + * {@code org.springframework.dao} exception hierarchy. + * + *

The central method is {@code execute}, supporting Hibernate access code + * implementing the {@link HibernateCallback} interface. It provides Hibernate Session + * handling such that neither the HibernateCallback implementation nor the calling + * code needs to explicitly care about retrieving/closing Hibernate Sessions, + * or handling Session lifecycle exceptions. For typical single step actions, + * there are various convenience methods (find, load, saveOrUpdate, delete). + * + *

Can be used within a service implementation via direct instantiation + * with a SessionFactory reference, or get prepared in an application context + * and given to services as bean reference. Note: The SessionFactory should + * always be configured as bean in the application context, in the first case + * given to the service directly, in the second case to the prepared template. + * + *

NOTE: Hibernate access code can also be coded in plain Hibernate style. + * Hence, for newly started projects, consider adopting the standard Hibernate + * style of coding data access objects instead, based on + * {@link SessionFactory#getCurrentSession()}. + * This HibernateTemplate primarily exists as a migration helper for Hibernate 3 + * based data access code, to benefit from bug fixes in Hibernate 4.x. + * + * @author Juergen Hoeller + * @since 4.2 + * @see #setSessionFactory + * @see HibernateCallback + * @see Session + * @see LocalSessionFactoryBean + * @see HibernateTransactionManager + * @see org.springframework.orm.hibernate5.support.OpenSessionInViewFilter + * @see org.springframework.orm.hibernate5.support.OpenSessionInViewInterceptor + */ +public class HibernateTemplate implements HibernateOperations, InitializingBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private SessionFactory sessionFactory; + + private String[] filterNames; + + private boolean exposeNativeSession = false; + + private boolean checkWriteOperations = true; + + private boolean cacheQueries = false; + + private String queryCacheRegion; + + private int fetchSize = 0; + + private int maxResults = 0; + + + /** + * Create a new HibernateTemplate instance. + */ + public HibernateTemplate() { + } + + /** + * Create a new HibernateTemplate instance. + * @param sessionFactory the SessionFactory to create Sessions with + */ + public HibernateTemplate(SessionFactory sessionFactory) { + setSessionFactory(sessionFactory); + afterPropertiesSet(); + } + + + /** + * Set the Hibernate SessionFactory that should be used to create + * Hibernate Sessions. + */ + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + /** + * Return the Hibernate SessionFactory that should be used to create + * Hibernate Sessions. + */ + public SessionFactory getSessionFactory() { + return this.sessionFactory; + } + + /** + * Set one or more names of Hibernate filters to be activated for all + * Sessions that this accessor works with. + *

Each of those filters will be enabled at the beginning of each + * operation and correspondingly disabled at the end of the operation. + * This will work for newly opened Sessions as well as for existing + * Sessions (for example, within a transaction). + * @see #enableFilters(Session) + * @see Session#enableFilter(String) + */ + public void setFilterNames(String... filterNames) { + this.filterNames = filterNames; + } + + /** + * Return the names of Hibernate filters to be activated, if any. + */ + public String[] getFilterNames() { + return this.filterNames; + } + + /** + * Set whether to expose the native Hibernate Session to + * HibernateCallback code. + *

Default is "false": a Session proxy will be returned, suppressing + * {@code close} calls and automatically applying query cache + * settings and transaction timeouts. + * @see HibernateCallback + * @see Session + * @see #setCacheQueries + * @see #setQueryCacheRegion + * @see #prepareQuery + * @see #prepareCriteria + */ + public void setExposeNativeSession(boolean exposeNativeSession) { + this.exposeNativeSession = exposeNativeSession; + } + + /** + * Return whether to expose the native Hibernate Session to + * HibernateCallback code, or rather a Session proxy. + */ + public boolean isExposeNativeSession() { + return this.exposeNativeSession; + } + + /** + * Set whether to check that the Hibernate Session is not in read-only mode + * in case of write operations (save/update/delete). + *

Default is "true", for fail-fast behavior when attempting write operations + * within a read-only transaction. Turn this off to allow save/update/delete + * on a Session with flush mode MANUAL. + * @see #checkWriteOperationAllowed + * @see org.springframework.transaction.TransactionDefinition#isReadOnly + */ + public void setCheckWriteOperations(boolean checkWriteOperations) { + this.checkWriteOperations = checkWriteOperations; + } + + /** + * Return whether to check that the Hibernate Session is not in read-only + * mode in case of write operations (save/update/delete). + */ + public boolean isCheckWriteOperations() { + return this.checkWriteOperations; + } + + /** + * Set whether to cache all queries executed by this template. + *

If this is "true", all Query and Criteria objects created by + * this template will be marked as cacheable (including all + * queries through find methods). + *

To specify the query region to be used for queries cached + * by this template, set the "queryCacheRegion" property. + * @see #setQueryCacheRegion + * @see Query#setCacheable + * @see Criteria#setCacheable + */ + public void setCacheQueries(boolean cacheQueries) { + this.cacheQueries = cacheQueries; + } + + /** + * Return whether to cache all queries executed by this template. + */ + public boolean isCacheQueries() { + return this.cacheQueries; + } + + /** + * Set the name of the cache region for queries executed by this template. + *

If this is specified, it will be applied to all Query and Criteria objects + * created by this template (including all queries through find methods). + *

The cache region will not take effect unless queries created by this + * template are configured to be cached via the "cacheQueries" property. + * @see #setCacheQueries + * @see Query#setCacheRegion + * @see Criteria#setCacheRegion + */ + public void setQueryCacheRegion(String queryCacheRegion) { + this.queryCacheRegion = queryCacheRegion; + } + + /** + * Return the name of the cache region for queries executed by this template. + */ + public String getQueryCacheRegion() { + return this.queryCacheRegion; + } + + /** + * Set the fetch size for this HibernateTemplate. This is important for processing + * large result sets: Setting this higher than the default value will increase + * processing speed at the cost of memory consumption; setting this lower can + * avoid transferring row data that will never be read by the application. + *

Default is 0, indicating to use the JDBC driver's default. + */ + public void setFetchSize(int fetchSize) { + this.fetchSize = fetchSize; + } + + /** + * Return the fetch size specified for this HibernateTemplate. + */ + public int getFetchSize() { + return this.fetchSize; + } + + /** + * Set the maximum number of rows for this HibernateTemplate. This is important + * for processing subsets of large result sets, avoiding to read and hold + * the entire result set in the database or in the JDBC driver if we're + * never interested in the entire result in the first place (for example, + * when performing searches that might return a large number of matches). + *

Default is 0, indicating to use the JDBC driver's default. + */ + public void setMaxResults(int maxResults) { + this.maxResults = maxResults; + } + + /** + * Return the maximum number of rows specified for this HibernateTemplate. + */ + public int getMaxResults() { + return this.maxResults; + } + + @Override + public void afterPropertiesSet() { + if (getSessionFactory() == null) { + throw new IllegalArgumentException("Property 'sessionFactory' is required"); + } + } + + + @Override + public T execute(HibernateCallback action) throws DataAccessException { + return doExecute(action, false); + } + + /** + * Execute the action specified by the given action object within a + * native {@link Session}. + *

This execute variant overrides the template-wide + * {@link #isExposeNativeSession() "exposeNativeSession"} setting. + * @param action callback object that specifies the Hibernate action + * @return a result object returned by the action, or {@code null} + * @throws DataAccessException in case of Hibernate errors + */ + public T executeWithNativeSession(HibernateCallback action) { + return doExecute(action, true); + } + + /** + * Execute the action specified by the given action object within a Session. + * @param action callback object that specifies the Hibernate action + * @param enforceNativeSession whether to enforce exposure of the native + * Hibernate Session to callback code + * @return a result object returned by the action, or {@code null} + * @throws DataAccessException in case of Hibernate errors + */ + protected T doExecute(HibernateCallback action, boolean enforceNativeSession) throws DataAccessException { + Assert.notNull(action, "Callback object must not be null"); + + Session session = null; + boolean isNew = false; + try { + session = getSessionFactory().getCurrentSession(); + } + catch (HibernateException ex) { + logger.debug("Could not retrieve pre-bound Hibernate session", ex); + } + if (session == null) { + session = getSessionFactory().openSession(); + session.setFlushMode(FlushMode.MANUAL); + isNew = true; + } + + try { + enableFilters(session); + Session sessionToExpose = + (enforceNativeSession || isExposeNativeSession() ? session : createSessionProxy(session)); + return action.doInHibernate(sessionToExpose); + } + catch (HibernateException ex) { + throw SessionFactoryUtils.convertHibernateAccessException(ex); + } + catch (RuntimeException ex) { + // Callback code threw application exception... + throw ex; + } + finally { + if (isNew) { + SessionFactoryUtils.closeSession(session); + } + else { + disableFilters(session); + } + } + } + + /** + * Create a close-suppressing proxy for the given Hibernate Session. + * The proxy also prepares returned Query and Criteria objects. + * @param session the Hibernate Session to create a proxy for + * @return the Session proxy + * @see Session#close() + * @see #prepareQuery + * @see #prepareCriteria + */ + protected Session createSessionProxy(Session session) { + return (Session) Proxy.newProxyInstance( + session.getClass().getClassLoader(), new Class[] {Session.class}, + new CloseSuppressingInvocationHandler(session)); + } + + /** + * Enable the specified filters on the given Session. + * @param session the current Hibernate Session + * @see #setFilterNames + * @see Session#enableFilter(String) + */ + protected void enableFilters(Session session) { + String[] filterNames = getFilterNames(); + if (filterNames != null) { + for (String filterName : filterNames) { + session.enableFilter(filterName); + } + } + } + + /** + * Disable the specified filters on the given Session. + * @param session the current Hibernate Session + * @see #setFilterNames + * @see Session#disableFilter(String) + */ + protected void disableFilters(Session session) { + String[] filterNames = getFilterNames(); + if (filterNames != null) { + for (String filterName : filterNames) { + session.disableFilter(filterName); + } + } + } + + + //------------------------------------------------------------------------- + // Convenience methods for loading individual objects + //------------------------------------------------------------------------- + + @Override + public T get(Class entityClass, Serializable id) throws DataAccessException { + return get(entityClass, id, null); + } + + @Override + public T get(final Class entityClass, final Serializable id, final LockMode lockMode) + throws DataAccessException { + + return executeWithNativeSession(new HibernateCallback() { + @Override + @SuppressWarnings("unchecked") + public T doInHibernate(Session session) throws HibernateException { + if (lockMode != null) { + return (T) session.get(entityClass, id, new LockOptions(lockMode)); + } + else { + return (T) session.get(entityClass, id); + } + } + }); + } + + @Override + public Object get(String entityName, Serializable id) throws DataAccessException { + return get(entityName, id, null); + } + + @Override + public Object get(final String entityName, final Serializable id, final LockMode lockMode) + throws DataAccessException { + + return executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + if (lockMode != null) { + return session.get(entityName, id, new LockOptions(lockMode)); + } + else { + return session.get(entityName, id); + } + } + }); + } + + @Override + public T load(Class entityClass, Serializable id) throws DataAccessException { + return load(entityClass, id, null); + } + + @Override + public T load(final Class entityClass, final Serializable id, final LockMode lockMode) + throws DataAccessException { + + return executeWithNativeSession(new HibernateCallback() { + @Override + @SuppressWarnings("unchecked") + public T doInHibernate(Session session) throws HibernateException { + if (lockMode != null) { + return (T) session.load(entityClass, id, new LockOptions(lockMode)); + } + else { + return (T) session.load(entityClass, id); + } + } + }); + } + + @Override + public Object load(String entityName, Serializable id) throws DataAccessException { + return load(entityName, id, null); + } + + @Override + public Object load(final String entityName, final Serializable id, final LockMode lockMode) + throws DataAccessException { + + return executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + if (lockMode != null) { + return session.load(entityName, id, new LockOptions(lockMode)); + } + else { + return session.load(entityName, id); + } + } + }); + } + + @Override + public List loadAll(final Class entityClass) throws DataAccessException { + return executeWithNativeSession(new HibernateCallback>() { + @Override + @SuppressWarnings("unchecked") + public List doInHibernate(Session session) throws HibernateException { + Criteria criteria = session.createCriteria(entityClass); + criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); + prepareCriteria(criteria); + return criteria.list(); + } + }); + } + + @Override + public void load(final Object entity, final Serializable id) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + session.load(entity, id); + return null; + } + }); + } + + @Override + public void refresh(final Object entity) throws DataAccessException { + refresh(entity, null); + } + + @Override + public void refresh(final Object entity, final LockMode lockMode) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + if (lockMode != null) { + session.refresh(entity, new LockOptions(lockMode)); + } + else { + session.refresh(entity); + } + return null; + } + }); + } + + @Override + public boolean contains(final Object entity) throws DataAccessException { + return executeWithNativeSession(new HibernateCallback() { + @Override + public Boolean doInHibernate(Session session) { + return session.contains(entity); + } + }); + } + + @Override + public void evict(final Object entity) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + session.evict(entity); + return null; + } + }); + } + + @Override + public void initialize(Object proxy) throws DataAccessException { + try { + Hibernate.initialize(proxy); + } + catch (HibernateException ex) { + throw SessionFactoryUtils.convertHibernateAccessException(ex); + } + } + + @Override + public Filter enableFilter(String filterName) throws IllegalStateException { + Session session = getSessionFactory().getCurrentSession(); + Filter filter = session.getEnabledFilter(filterName); + if (filter == null) { + filter = session.enableFilter(filterName); + } + return filter; + } + + + //------------------------------------------------------------------------- + // Convenience methods for storing individual objects + //------------------------------------------------------------------------- + + @Override + public void lock(final Object entity, final LockMode lockMode) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + session.buildLockRequest(new LockOptions(lockMode)).lock(entity); + return null; + } + }); + } + + @Override + public void lock(final String entityName, final Object entity, final LockMode lockMode) + throws DataAccessException { + + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + session.buildLockRequest(new LockOptions(lockMode)).lock(entityName, entity); + return null; + } + }); + } + + @Override + public Serializable save(final Object entity) throws DataAccessException { + return executeWithNativeSession(new HibernateCallback() { + @Override + public Serializable doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + return session.save(entity); + } + }); + } + + @Override + public Serializable save(final String entityName, final Object entity) throws DataAccessException { + return executeWithNativeSession(new HibernateCallback() { + @Override + public Serializable doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + return session.save(entityName, entity); + } + }); + } + + @Override + public void update(Object entity) throws DataAccessException { + update(entity, null); + } + + @Override + public void update(final Object entity, final LockMode lockMode) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.update(entity); + if (lockMode != null) { + session.buildLockRequest(new LockOptions(lockMode)).lock(entity); + } + return null; + } + }); + } + + @Override + public void update(String entityName, Object entity) throws DataAccessException { + update(entityName, entity, null); + } + + @Override + public void update(final String entityName, final Object entity, final LockMode lockMode) + throws DataAccessException { + + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.update(entityName, entity); + if (lockMode != null) { + session.buildLockRequest(new LockOptions(lockMode)).lock(entityName, entity); + } + return null; + } + }); + } + + @Override + public void saveOrUpdate(final Object entity) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.saveOrUpdate(entity); + return null; + } + }); + } + + @Override + public void saveOrUpdate(final String entityName, final Object entity) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.saveOrUpdate(entityName, entity); + return null; + } + }); + } + + @Override + public void replicate(final Object entity, final ReplicationMode replicationMode) + throws DataAccessException { + + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.replicate(entity, replicationMode); + return null; + } + }); + } + + @Override + public void replicate(final String entityName, final Object entity, final ReplicationMode replicationMode) + throws DataAccessException { + + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.replicate(entityName, entity, replicationMode); + return null; + } + }); + } + + @Override + public void persist(final Object entity) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.persist(entity); + return null; + } + }); + } + + @Override + public void persist(final String entityName, final Object entity) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + session.persist(entityName, entity); + return null; + } + }); + } + + @Override + public T merge(final T entity) throws DataAccessException { + return executeWithNativeSession(new HibernateCallback() { + @Override + @SuppressWarnings("unchecked") + public T doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + return (T) session.merge(entity); + } + }); + } + + @Override + public T merge(final String entityName, final T entity) throws DataAccessException { + return executeWithNativeSession(new HibernateCallback() { + @Override + @SuppressWarnings("unchecked") + public T doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + return (T) session.merge(entityName, entity); + } + }); + } + + @Override + public void delete(Object entity) throws DataAccessException { + delete(entity, null); + } + + @Override + public void delete(final Object entity, final LockMode lockMode) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + if (lockMode != null) { + session.buildLockRequest(new LockOptions(lockMode)).lock(entity); + } + session.delete(entity); + return null; + } + }); + } + + @Override + public void delete(String entityName, Object entity) throws DataAccessException { + delete(entityName, entity, null); + } + + @Override + public void delete(final String entityName, final Object entity, final LockMode lockMode) + throws DataAccessException { + + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + if (lockMode != null) { + session.buildLockRequest(new LockOptions(lockMode)).lock(entityName, entity); + } + session.delete(entityName, entity); + return null; + } + }); + } + + @Override + public void deleteAll(final Collection entities) throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + checkWriteOperationAllowed(session); + for (Object entity : entities) { + session.delete(entity); + } + return null; + } + }); + } + + @Override + public void flush() throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) throws HibernateException { + session.flush(); + return null; + } + }); + } + + @Override + public void clear() throws DataAccessException { + executeWithNativeSession(new HibernateCallback() { + @Override + public Object doInHibernate(Session session) { + session.clear(); + return null; + } + }); + } + + + //------------------------------------------------------------------------- + // Convenience finder methods for HQL strings + //------------------------------------------------------------------------- + + @Override + public List find(final String queryString, final Object... values) throws DataAccessException { + return executeWithNativeSession(new HibernateCallback>() { + @Override + @SuppressWarnings("unchecked") + public List doInHibernate(Session session) throws HibernateException { + Query queryObject = session.createQuery(queryString); + prepareQuery(queryObject); + if (values != null) { + for (int i = 0; i < values.length; i++) { + queryObject.setParameter(i, values[i]); + } + } + return queryObject.list(); + } + }); + } + + @Override + public List findByNamedParam(String queryString, String paramName, Object value) + throws DataAccessException { + + return findByNamedParam(queryString, new String[] {paramName}, new Object[] {value}); + } + + @Override + public List findByNamedParam(final String queryString, final String[] paramNames, final Object[] values) + throws DataAccessException { + + if (paramNames.length != values.length) { + throw new IllegalArgumentException("Length of paramNames array must match length of values array"); + } + return executeWithNativeSession(new HibernateCallback>() { + @Override + @SuppressWarnings("unchecked") + public List doInHibernate(Session session) throws HibernateException { + Query queryObject = session.createQuery(queryString); + prepareQuery(queryObject); + if (values != null) { + for (int i = 0; i < values.length; i++) { + applyNamedParameterToQuery(queryObject, paramNames[i], values[i]); + } + } + return queryObject.list(); + } + }); + } + + @Override + public List findByValueBean(final String queryString, final Object valueBean) + throws DataAccessException { + + return executeWithNativeSession(new HibernateCallback>() { + @Override + @SuppressWarnings("unchecked") + public List doInHibernate(Session session) throws HibernateException { + Query queryObject = session.createQuery(queryString); + prepareQuery(queryObject); + queryObject.setProperties(valueBean); + return queryObject.list(); + } + }); + } + + + //------------------------------------------------------------------------- + // Convenience finder methods for named queries + //------------------------------------------------------------------------- + + @Override + public List findByNamedQuery(final String queryName, final Object... values) throws DataAccessException { + return executeWithNativeSession(new HibernateCallback>() { + @Override + @SuppressWarnings("unchecked") + public List doInHibernate(Session session) throws HibernateException { + Query queryObject = session.getNamedQuery(queryName); + prepareQuery(queryObject); + if (values != null) { + for (int i = 0; i < values.length; i++) { + queryObject.setParameter(i, values[i]); + } + } + return queryObject.list(); + } + }); + } + + @Override + public List findByNamedQueryAndNamedParam(String queryName, String paramName, Object value) + throws DataAccessException { + + return findByNamedQueryAndNamedParam(queryName, new String[] {paramName}, new Object[] {value}); + } + + @Override + public List findByNamedQueryAndNamedParam( + final String queryName, final String[] paramNames, final Object[] values) + throws DataAccessException { + + if (values != null && (paramNames == null || paramNames.length != values.length)) { + throw new IllegalArgumentException("Length of paramNames array must match length of values array"); + } + return executeWithNativeSession(new HibernateCallback>() { + @Override + @SuppressWarnings("unchecked") + public List doInHibernate(Session session) throws HibernateException { + Query queryObject = session.getNamedQuery(queryName); + prepareQuery(queryObject); + if (values != null) { + for (int i = 0; i < values.length; i++) { + applyNamedParameterToQuery(queryObject, paramNames[i], values[i]); + } + } + return queryObject.list(); + } + }); + } + + @Override + public List findByNamedQueryAndValueBean(final String queryName, final Object valueBean) + throws DataAccessException { + + return executeWithNativeSession(new HibernateCallback>() { + @Override + @SuppressWarnings("unchecked") + public List doInHibernate(Session session) throws HibernateException { + Query queryObject = session.getNamedQuery(queryName); + prepareQuery(queryObject); + queryObject.setProperties(valueBean); + return queryObject.list(); + } + }); + } + + + //------------------------------------------------------------------------- + // Convenience finder methods for detached criteria + //------------------------------------------------------------------------- + + @Override + public List findByCriteria(DetachedCriteria criteria) throws DataAccessException { + return findByCriteria(criteria, -1, -1); + } + + @Override + public List findByCriteria(final DetachedCriteria criteria, final int firstResult, final int maxResults) + throws DataAccessException { + + Assert.notNull(criteria, "DetachedCriteria must not be null"); + return executeWithNativeSession(new HibernateCallback>() { + @Override + @SuppressWarnings("unchecked") + public List doInHibernate(Session session) throws HibernateException { + Criteria executableCriteria = criteria.getExecutableCriteria(session); + prepareCriteria(executableCriteria); + if (firstResult >= 0) { + executableCriteria.setFirstResult(firstResult); + } + if (maxResults > 0) { + executableCriteria.setMaxResults(maxResults); + } + return executableCriteria.list(); + } + }); + } + + @Override + public List findByExample(T exampleEntity) throws DataAccessException { + return findByExample(null, exampleEntity, -1, -1); + } + + @Override + public List findByExample(String entityName, T exampleEntity) throws DataAccessException { + return findByExample(entityName, exampleEntity, -1, -1); + } + + @Override + public List findByExample(T exampleEntity, int firstResult, int maxResults) throws DataAccessException { + return findByExample(null, exampleEntity, firstResult, maxResults); + } + + @Override + public List findByExample( + final String entityName, final T exampleEntity, final int firstResult, final int maxResults) + throws DataAccessException { + + Assert.notNull(exampleEntity, "Example entity must not be null"); + return executeWithNativeSession(new HibernateCallback>() { + @Override + @SuppressWarnings("unchecked") + public List doInHibernate(Session session) throws HibernateException { + Criteria executableCriteria = (entityName != null ? + session.createCriteria(entityName) : session.createCriteria(exampleEntity.getClass())); + executableCriteria.add(Example.create(exampleEntity)); + prepareCriteria(executableCriteria); + if (firstResult >= 0) { + executableCriteria.setFirstResult(firstResult); + } + if (maxResults > 0) { + executableCriteria.setMaxResults(maxResults); + } + return executableCriteria.list(); + } + }); + } + + + //------------------------------------------------------------------------- + // Convenience query methods for iteration and bulk updates/deletes + //------------------------------------------------------------------------- + + @Override + public Iterator iterate(final String queryString, final Object... values) throws DataAccessException { + return executeWithNativeSession(new HibernateCallback>() { + @Override + @SuppressWarnings("unchecked") + public Iterator doInHibernate(Session session) throws HibernateException { + Query queryObject = session.createQuery(queryString); + prepareQuery(queryObject); + if (values != null) { + for (int i = 0; i < values.length; i++) { + queryObject.setParameter(i, values[i]); + } + } + return queryObject.iterate(); + } + }); + } + + @Override + public void closeIterator(Iterator it) throws DataAccessException { + try { + Hibernate.close(it); + } + catch (HibernateException ex) { + throw SessionFactoryUtils.convertHibernateAccessException(ex); + } + } + + @Override + public int bulkUpdate(final String queryString, final Object... values) throws DataAccessException { + return executeWithNativeSession(new HibernateCallback() { + @Override + public Integer doInHibernate(Session session) throws HibernateException { + Query queryObject = session.createQuery(queryString); + prepareQuery(queryObject); + if (values != null) { + for (int i = 0; i < values.length; i++) { + queryObject.setParameter(i, values[i]); + } + } + return queryObject.executeUpdate(); + } + }); + } + + + //------------------------------------------------------------------------- + // Helper methods used by the operations above + //------------------------------------------------------------------------- + + /** + * Check whether write operations are allowed on the given Session. + *

Default implementation throws an InvalidDataAccessApiUsageException in + * case of {@code FlushMode.MANUAL}. Can be overridden in subclasses. + * @param session current Hibernate Session + * @throws InvalidDataAccessApiUsageException if write operations are not allowed + * @see #setCheckWriteOperations + * @see Session#getFlushMode() + * @see FlushMode#MANUAL + */ + protected void checkWriteOperationAllowed(Session session) throws InvalidDataAccessApiUsageException { + if (isCheckWriteOperations() && session.getFlushMode().lessThan(FlushMode.COMMIT)) { + throw new InvalidDataAccessApiUsageException( + "Write operations are not allowed in read-only mode (FlushMode.MANUAL): "+ + "Turn your Session into FlushMode.COMMIT/AUTO or remove 'readOnly' marker from transaction definition."); + } + } + + /** + * Prepare the given Query object, applying cache settings and/or + * a transaction timeout. + * @param queryObject the Query object to prepare + * @see #setCacheQueries + * @see #setQueryCacheRegion + */ + protected void prepareQuery(Query queryObject) { + if (isCacheQueries()) { + queryObject.setCacheable(true); + if (getQueryCacheRegion() != null) { + queryObject.setCacheRegion(getQueryCacheRegion()); + } + } + if (getFetchSize() > 0) { + queryObject.setFetchSize(getFetchSize()); + } + if (getMaxResults() > 0) { + queryObject.setMaxResults(getMaxResults()); + } + + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.getResource(getSessionFactory()); + if (sessionHolder != null && sessionHolder.hasTimeout()) { + queryObject.setTimeout(sessionHolder.getTimeToLiveInSeconds()); + } + } + + /** + * Prepare the given Criteria object, applying cache settings and/or + * a transaction timeout. + * @param criteria the Criteria object to prepare + * @see #setCacheQueries + * @see #setQueryCacheRegion + */ + protected void prepareCriteria(Criteria criteria) { + if (isCacheQueries()) { + criteria.setCacheable(true); + if (getQueryCacheRegion() != null) { + criteria.setCacheRegion(getQueryCacheRegion()); + } + } + if (getFetchSize() > 0) { + criteria.setFetchSize(getFetchSize()); + } + if (getMaxResults() > 0) { + criteria.setMaxResults(getMaxResults()); + } + + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.getResource(getSessionFactory()); + if (sessionHolder != null && sessionHolder.hasTimeout()) { + criteria.setTimeout(sessionHolder.getTimeToLiveInSeconds()); + } + } + + /** + * Apply the given name parameter to the given Query object. + * @param queryObject the Query object + * @param paramName the name of the parameter + * @param value the value of the parameter + * @throws HibernateException if thrown by the Query object + */ + protected void applyNamedParameterToQuery(Query queryObject, String paramName, Object value) + throws HibernateException { + + if (value instanceof Collection) { + queryObject.setParameterList(paramName, (Collection) value); + } + else if (value instanceof Object[]) { + queryObject.setParameterList(paramName, (Object[]) value); + } + else { + queryObject.setParameter(paramName, value); + } + } + + + /** + * Invocation handler that suppresses close calls on Hibernate Sessions. + * Also prepares returned Query and Criteria objects. + * @see Session#close + */ + private class CloseSuppressingInvocationHandler implements InvocationHandler { + + private final Session target; + + public CloseSuppressingInvocationHandler(Session target) { + this.target = target; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on Session interface coming in... + + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0]); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of Session proxy. + return System.identityHashCode(proxy); + } + else if (method.getName().equals("close")) { + // Handle close method: suppress, not valid. + return null; + } + + // Invoke method on target Session. + try { + Object retVal = method.invoke(this.target, args); + + // If return value is a Query or Criteria, apply transaction timeout. + // Applies to createQuery, getNamedQuery, createCriteria. + if (retVal instanceof Query) { + prepareQuery(((Query) retVal)); + } + if (retVal instanceof Criteria) { + prepareCriteria(((Criteria) retVal)); + } + + return retVal; + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java new file mode 100644 index 0000000000..4bf04c983d --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java @@ -0,0 +1,839 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import java.sql.Connection; +import java.sql.ResultSet; +import javax.sql.DataSource; + +import org.hibernate.ConnectionReleaseMode; +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Interceptor; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.resource.transaction.spi.TransactionStatus; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.datasource.ConnectionHolder; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.jdbc.datasource.JdbcTransactionObjectSupport; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.InvalidIsolationLevelException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * {@link org.springframework.transaction.PlatformTransactionManager} + * implementation for a single Hibernate {@link SessionFactory}. + * Binds a Hibernate Session from the specified factory to the thread, + * potentially allowing for one thread-bound Session per factory. + * {@code SessionFactory.getCurrentSession()} is required for Hibernate + * access code that needs to support this transaction handling mechanism, + * with the SessionFactory being configured with {@link SpringSessionContext}. + * + *

Supports custom isolation levels, and timeouts that get applied as + * Hibernate transaction timeouts. + * + *

This transaction manager is appropriate for applications that use a single + * Hibernate SessionFactory for transactional data access, but it also supports + * direct DataSource access within a transaction (i.e. plain JDBC code working + * with the same DataSource). This allows for mixing services which access Hibernate + * and services which use plain JDBC (without being aware of Hibernate)! + * Application code needs to stick to the same simple Connection lookup pattern as + * with {@link org.springframework.jdbc.datasource.DataSourceTransactionManager} + * (i.e. {@link DataSourceUtils#getConnection} + * or going through a + * {@link TransactionAwareDataSourceProxy}). + * + *

Note: To be able to register a DataSource's Connection for plain JDBC code, + * this instance needs to be aware of the DataSource ({@link #setDataSource}). + * The given DataSource should obviously match the one used by the given SessionFactory. + * + *

JTA (usually through {@link org.springframework.transaction.jta.JtaTransactionManager}) + * is necessary for accessing multiple transactional resources within the same + * transaction. The DataSource that Hibernate uses needs to be JTA-enabled in + * such a scenario (see container setup). + * + *

This transaction manager supports nested transactions via JDBC 3.0 Savepoints. + * The {@link #setNestedTransactionAllowed} "nestedTransactionAllowed"} flag defaults + * to "false", though, as nested transactions will just apply to the JDBC Connection, + * not to the Hibernate Session and its cached entity objects and related context. + * You can manually set the flag to "true" if you want to use nested transactions + * for JDBC access code which participates in Hibernate transactions (provided that + * your JDBC driver supports Savepoints). Note that Hibernate itself does not + * support nested transactions! Hence, do not expect Hibernate access code to + * semantically participate in a nested transaction. + * + *

NOTE: Hibernate 4.2+ is strongly recommended for efficient transaction + * management with Spring, in particular for transactional Spring JDBC access. + * + * @author Juergen Hoeller + * @since 4.2 + * @see #setSessionFactory + * @see #setDataSource + * @see SessionFactory#getCurrentSession() + * @see DataSourceUtils#getConnection + * @see DataSourceUtils#releaseConnection + * @see org.springframework.jdbc.core.JdbcTemplate + * @see org.springframework.jdbc.datasource.DataSourceTransactionManager + * @see org.springframework.transaction.jta.JtaTransactionManager + */ +@SuppressWarnings("serial") +public class HibernateTransactionManager extends AbstractPlatformTransactionManager + implements ResourceTransactionManager, BeanFactoryAware, InitializingBean { + + private SessionFactory sessionFactory; + + private DataSource dataSource; + + private boolean autodetectDataSource = true; + + private boolean prepareConnection = true; + + private boolean allowResultAccessAfterCompletion = false; + + private boolean hibernateManagedSession = false; + + private Object entityInterceptor; + + /** + * Just needed for entityInterceptorBeanName. + * @see #setEntityInterceptorBeanName + */ + private BeanFactory beanFactory; + + + /** + * Create a new HibernateTransactionManager instance. + * A SessionFactory has to be set to be able to use it. + * @see #setSessionFactory + */ + public HibernateTransactionManager() { + } + + /** + * Create a new HibernateTransactionManager instance. + * @param sessionFactory SessionFactory to manage transactions for + */ + public HibernateTransactionManager(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + afterPropertiesSet(); + } + + + /** + * Set the SessionFactory that this instance should manage transactions for. + */ + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + /** + * Return the SessionFactory that this instance should manage transactions for. + */ + public SessionFactory getSessionFactory() { + return this.sessionFactory; + } + + /** + * Set the JDBC DataSource that this instance should manage transactions for. + * The DataSource should match the one used by the Hibernate SessionFactory: + * for example, you could specify the same JNDI DataSource for both. + *

If the SessionFactory was configured with LocalDataSourceConnectionProvider, + * i.e. by Spring's LocalSessionFactoryBean with a specified "dataSource", + * the DataSource will be auto-detected: You can still explicitly specify the + * DataSource, but you don't need to in this case. + *

A transactional JDBC Connection for this DataSource will be provided to + * application code accessing this DataSource directly via DataSourceUtils + * or JdbcTemplate. The Connection will be taken from the Hibernate Session. + *

The DataSource specified here should be the target DataSource to manage + * transactions for, not a TransactionAwareDataSourceProxy. Only data access + * code may work with TransactionAwareDataSourceProxy, while the transaction + * manager needs to work on the underlying target DataSource. If there's + * nevertheless a TransactionAwareDataSourceProxy passed in, it will be + * unwrapped to extract its target DataSource. + * @see #setAutodetectDataSource + * @see TransactionAwareDataSourceProxy + * @see DataSourceUtils + * @see org.springframework.jdbc.core.JdbcTemplate + */ + public void setDataSource(DataSource dataSource) { + if (dataSource instanceof TransactionAwareDataSourceProxy) { + // If we got a TransactionAwareDataSourceProxy, we need to perform transactions + // for its underlying target DataSource, else data access code won't see + // properly exposed transactions (i.e. transactions for the target DataSource). + this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource(); + } + else { + this.dataSource = dataSource; + } + } + + /** + * Return the JDBC DataSource that this instance manages transactions for. + */ + public DataSource getDataSource() { + return this.dataSource; + } + + /** + * Set whether to autodetect a JDBC DataSource used by the Hibernate SessionFactory, + * if set via LocalSessionFactoryBean's {@code setDataSource}. Default is "true". + *

Can be turned off to deliberately ignore an available DataSource, in order + * to not expose Hibernate transactions as JDBC transactions for that DataSource. + * @see #setDataSource + */ + public void setAutodetectDataSource(boolean autodetectDataSource) { + this.autodetectDataSource = autodetectDataSource; + } + + /** + * Set whether to prepare the underlying JDBC Connection of a transactional + * Hibernate Session, that is, whether to apply a transaction-specific + * isolation level and/or the transaction's read-only flag to the underlying + * JDBC Connection. + *

Default is "true". If you turn this flag off, the transaction manager + * will not support per-transaction isolation levels anymore. It will not + * call {@code Connection.setReadOnly(true)} for read-only transactions + * anymore either. If this flag is turned off, no cleanup of a JDBC Connection + * is required after a transaction, since no Connection settings will get modified. + * @see Connection#setTransactionIsolation + * @see Connection#setReadOnly + */ + public void setPrepareConnection(boolean prepareConnection) { + this.prepareConnection = prepareConnection; + } + + /** + * Set whether to allow result access after completion, typically via Hibernate's + * ScrollableResults mechanism. + *

Default is "false". Turning this flag on enforces over-commit holdability on the + * underlying JDBC Connection (if {@link #prepareConnection "prepareConnection"} is on) + * and skips the disconnect-on-completion step. + * @since 4.2 + * @see Connection#setHoldability + * @see ResultSet#HOLD_CURSORS_OVER_COMMIT + * @see #disconnectOnCompletion(Session) + */ + public void setAllowResultAccessAfterCompletion(boolean allowResultAccessAfterCompletion) { + this.allowResultAccessAfterCompletion = allowResultAccessAfterCompletion; + } + + /** + * Set whether to operate on a Hibernate-managed Session instead of a + * Spring-managed Session, that is, whether to obtain the Session through + * Hibernate's {@link SessionFactory#getCurrentSession()} + * instead of {@link SessionFactory#openSession()} (with a Spring + * {@link TransactionSynchronizationManager} + * check preceding it). + *

Default is "false", i.e. using a Spring-managed Session: taking the current + * thread-bound Session if available (e.g. in an Open-Session-in-View scenario), + * creating a new Session for the current transaction otherwise. + *

Switch this flag to "true" in order to enforce use of a Hibernate-managed Session. + * Note that this requires {@link SessionFactory#getCurrentSession()} + * to always return a proper Session when called for a Spring-managed transaction; + * transaction begin will fail if the {@code getCurrentSession()} call fails. + *

This mode will typically be used in combination with a custom Hibernate + * {@link org.hibernate.context.spi.CurrentSessionContext} implementation that stores + * Sessions in a place other than Spring's TransactionSynchronizationManager. + * It may also be used in combination with Spring's Open-Session-in-View support + * (using Spring's default {@link SpringSessionContext}), in which case it subtly + * differs from the Spring-managed Session mode: The pre-bound Session will not + * receive a {@code clear()} call (on rollback) or a {@code disconnect()} + * call (on transaction completion) in such a scenario; this is rather left up + * to a custom CurrentSessionContext implementation (if desired). + */ + public void setHibernateManagedSession(boolean hibernateManagedSession) { + this.hibernateManagedSession = hibernateManagedSession; + } + + /** + * Set the bean name of a Hibernate entity interceptor that allows to inspect + * and change property values before writing to and reading from the database. + * Will get applied to any new Session created by this transaction manager. + *

Requires the bean factory to be known, to be able to resolve the bean + * name to an interceptor instance on session creation. Typically used for + * prototype interceptors, i.e. a new interceptor instance per session. + *

Can also be used for shared interceptor instances, but it is recommended + * to set the interceptor reference directly in such a scenario. + * @param entityInterceptorBeanName the name of the entity interceptor in + * the bean factory + * @see #setBeanFactory + * @see #setEntityInterceptor + */ + public void setEntityInterceptorBeanName(String entityInterceptorBeanName) { + this.entityInterceptor = entityInterceptorBeanName; + } + + /** + * Set a Hibernate entity interceptor that allows to inspect and change + * property values before writing to and reading from the database. + * Will get applied to any new Session created by this transaction manager. + *

Such an interceptor can either be set at the SessionFactory level, + * i.e. on LocalSessionFactoryBean, or at the Session level, i.e. on + * HibernateTransactionManager. + * @see LocalSessionFactoryBean#setEntityInterceptor + */ + public void setEntityInterceptor(Interceptor entityInterceptor) { + this.entityInterceptor = entityInterceptor; + } + + /** + * Return the current Hibernate entity interceptor, or {@code null} if none. + * Resolves an entity interceptor bean name via the bean factory, + * if necessary. + * @throws IllegalStateException if bean name specified but no bean factory set + * @throws BeansException if bean name resolution via the bean factory failed + * @see #setEntityInterceptor + * @see #setEntityInterceptorBeanName + * @see #setBeanFactory + */ + public Interceptor getEntityInterceptor() throws IllegalStateException, BeansException { + if (this.entityInterceptor instanceof Interceptor) { + return (Interceptor) entityInterceptor; + } + else if (this.entityInterceptor instanceof String) { + if (this.beanFactory == null) { + throw new IllegalStateException("Cannot get entity interceptor via bean name if no bean factory set"); + } + String beanName = (String) this.entityInterceptor; + return this.beanFactory.getBean(beanName, Interceptor.class); + } + else { + return null; + } + } + + /** + * The bean factory just needs to be known for resolving entity interceptor + * bean names. It does not need to be set for any other mode of operation. + * @see #setEntityInterceptorBeanName + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public void afterPropertiesSet() { + if (getSessionFactory() == null) { + throw new IllegalArgumentException("Property 'sessionFactory' is required"); + } + if (this.entityInterceptor instanceof String && this.beanFactory == null) { + throw new IllegalArgumentException("Property 'beanFactory' is required for 'entityInterceptorBeanName'"); + } + + // Check for SessionFactory's DataSource. + if (this.autodetectDataSource && getDataSource() == null) { + DataSource sfds = SessionFactoryUtils.getDataSource(getSessionFactory()); + if (sfds != null) { + // Use the SessionFactory's DataSource for exposing transactions to JDBC code. + if (logger.isInfoEnabled()) { + logger.info("Using DataSource [" + sfds + + "] of Hibernate SessionFactory for HibernateTransactionManager"); + } + setDataSource(sfds); + } + } + } + + + @Override + public Object getResourceFactory() { + return getSessionFactory(); + } + + @Override + protected Object doGetTransaction() { + HibernateTransactionObject txObject = new HibernateTransactionObject(); + txObject.setSavepointAllowed(isNestedTransactionAllowed()); + + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.getResource(getSessionFactory()); + if (sessionHolder != null) { + if (logger.isDebugEnabled()) { + logger.debug("Found thread-bound Session [" + sessionHolder.getSession() + "] for Hibernate transaction"); + } + txObject.setSessionHolder(sessionHolder); + } + else if (this.hibernateManagedSession) { + try { + Session session = this.sessionFactory.getCurrentSession(); + if (logger.isDebugEnabled()) { + logger.debug("Found Hibernate-managed Session [" + session + "] for Spring-managed transaction"); + } + txObject.setExistingSession(session); + } + catch (HibernateException ex) { + throw new DataAccessResourceFailureException( + "Could not obtain Hibernate-managed Session for Spring-managed transaction", ex); + } + } + + if (getDataSource() != null) { + ConnectionHolder conHolder = (ConnectionHolder) + TransactionSynchronizationManager.getResource(getDataSource()); + txObject.setConnectionHolder(conHolder); + } + + return txObject; + } + + @Override + protected boolean isExistingTransaction(Object transaction) { + HibernateTransactionObject txObject = (HibernateTransactionObject) transaction; + return (txObject.hasSpringManagedTransaction() || + (this.hibernateManagedSession && txObject.hasHibernateManagedTransaction())); + } + + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) { + HibernateTransactionObject txObject = (HibernateTransactionObject) transaction; + + if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + throw new IllegalTransactionStateException( + "Pre-bound JDBC Connection found! HibernateTransactionManager does not support " + + "running within DataSourceTransactionManager if told to manage the DataSource itself. " + + "It is recommended to use a single HibernateTransactionManager for all transactions " + + "on a single DataSource, no matter whether Hibernate or JDBC access."); + } + + Session session = null; + + try { + if (txObject.getSessionHolder() == null || txObject.getSessionHolder().isSynchronizedWithTransaction()) { + Interceptor entityInterceptor = getEntityInterceptor(); + Session newSession = (entityInterceptor != null ? + getSessionFactory().withOptions().interceptor(entityInterceptor).openSession() : + getSessionFactory().openSession()); + if (logger.isDebugEnabled()) { + logger.debug("Opened new Session [" + newSession + "] for Hibernate transaction"); + } + txObject.setSession(newSession); + } + + session = txObject.getSessionHolder().getSession(); + + if (this.prepareConnection && isSameConnectionForEntireSession(session)) { + // We're allowed to change the transaction settings of the JDBC Connection. + if (logger.isDebugEnabled()) { + logger.debug("Preparing JDBC Connection of Hibernate Session [" + session + "]"); + } + Connection con = ((SessionImplementor) session).connection(); + Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); + txObject.setPreviousIsolationLevel(previousIsolationLevel); + if (this.allowResultAccessAfterCompletion && !txObject.isNewSession()) { + int currentHoldability = con.getHoldability(); + if (currentHoldability != ResultSet.HOLD_CURSORS_OVER_COMMIT) { + txObject.setPreviousHoldability(currentHoldability); + con.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT); + } + } + } + else { + // Not allowed to change the transaction settings of the JDBC Connection. + if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + // We should set a specific isolation level but are not allowed to... + throw new InvalidIsolationLevelException( + "HibernateTransactionManager is not allowed to support custom isolation levels: " + + "make sure that its 'prepareConnection' flag is on (the default) and that the " + + "Hibernate connection release mode is set to 'on_close' (the default for JDBC)."); + } + if (logger.isDebugEnabled()) { + logger.debug("Not preparing JDBC Connection of Hibernate Session [" + session + "]"); + } + } + + if (definition.isReadOnly() && txObject.isNewSession()) { + // Just set to MANUAL in case of a new Session for this transaction. + session.setFlushMode(FlushMode.MANUAL); + } + + if (!definition.isReadOnly() && !txObject.isNewSession()) { + // We need AUTO or COMMIT for a non-read-only transaction. + FlushMode flushMode = session.getFlushMode(); + if (session.getFlushMode().equals(FlushMode.MANUAL)) { + session.setFlushMode(FlushMode.AUTO); + txObject.getSessionHolder().setPreviousFlushMode(flushMode); + } + } + + Transaction hibTx; + + // Register transaction timeout. + int timeout = determineTimeout(definition); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + // Use Hibernate's own transaction timeout mechanism on Hibernate 3.1+ + // Applies to all statements, also to inserts, updates and deletes! + hibTx = session.getTransaction(); + hibTx.setTimeout(timeout); + hibTx.begin(); + } + else { + // Open a plain Hibernate transaction without specified timeout. + hibTx = session.beginTransaction(); + } + + // Add the Hibernate transaction to the session holder. + txObject.getSessionHolder().setTransaction(hibTx); + + // Register the Hibernate Session's JDBC Connection for the DataSource, if set. + if (getDataSource() != null) { + Connection con = ((SessionImplementor) session).connection(); + ConnectionHolder conHolder = new ConnectionHolder(con); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + conHolder.setTimeoutInSeconds(timeout); + } + if (logger.isDebugEnabled()) { + logger.debug("Exposing Hibernate transaction as JDBC transaction [" + con + "]"); + } + TransactionSynchronizationManager.bindResource(getDataSource(), conHolder); + txObject.setConnectionHolder(conHolder); + } + + // Bind the session holder to the thread. + if (txObject.isNewSessionHolder()) { + TransactionSynchronizationManager.bindResource(getSessionFactory(), txObject.getSessionHolder()); + } + txObject.getSessionHolder().setSynchronizedWithTransaction(true); + } + + catch (Throwable ex) { + if (txObject.isNewSession()) { + try { + if (session.getTransaction().getStatus() == TransactionStatus.ACTIVE) { + session.getTransaction().rollback(); + } + } + catch (Throwable ex2) { + logger.debug("Could not rollback Session after failed transaction begin", ex); + } + finally { + SessionFactoryUtils.closeSession(session); + txObject.setSessionHolder(null); + } + } + throw new CannotCreateTransactionException("Could not open Hibernate Session for transaction", ex); + } + } + + @Override + protected Object doSuspend(Object transaction) { + HibernateTransactionObject txObject = (HibernateTransactionObject) transaction; + txObject.setSessionHolder(null); + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.unbindResource(getSessionFactory()); + txObject.setConnectionHolder(null); + ConnectionHolder connectionHolder = null; + if (getDataSource() != null) { + connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.unbindResource(getDataSource()); + } + return new SuspendedResourcesHolder(sessionHolder, connectionHolder); + } + + @Override + protected void doResume(Object transaction, Object suspendedResources) { + SuspendedResourcesHolder resourcesHolder = (SuspendedResourcesHolder) suspendedResources; + if (TransactionSynchronizationManager.hasResource(getSessionFactory())) { + // From non-transactional code running in active transaction synchronization + // -> can be safely removed, will be closed on transaction completion. + TransactionSynchronizationManager.unbindResource(getSessionFactory()); + } + TransactionSynchronizationManager.bindResource(getSessionFactory(), resourcesHolder.getSessionHolder()); + if (getDataSource() != null) { + TransactionSynchronizationManager.bindResource(getDataSource(), resourcesHolder.getConnectionHolder()); + } + } + + @Override + protected void doCommit(DefaultTransactionStatus status) { + HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Committing Hibernate transaction on Session [" + + txObject.getSessionHolder().getSession() + "]"); + } + try { + txObject.getSessionHolder().getTransaction().commit(); + } + catch (org.hibernate.TransactionException ex) { + // assumably from commit call to the underlying JDBC connection + throw new TransactionSystemException("Could not commit Hibernate transaction", ex); + } + catch (HibernateException ex) { + // assumably failed to flush changes to database + throw convertHibernateAccessException(ex); + } + } + + @Override + protected void doRollback(DefaultTransactionStatus status) { + HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Rolling back Hibernate transaction on Session [" + + txObject.getSessionHolder().getSession() + "]"); + } + try { + txObject.getSessionHolder().getTransaction().rollback(); + } + catch (org.hibernate.TransactionException ex) { + throw new TransactionSystemException("Could not roll back Hibernate transaction", ex); + } + catch (HibernateException ex) { + // Shouldn't really happen, as a rollback doesn't cause a flush. + throw convertHibernateAccessException(ex); + } + finally { + if (!txObject.isNewSession() && !this.hibernateManagedSession) { + // Clear all pending inserts/updates/deletes in the Session. + // Necessary for pre-bound Sessions, to avoid inconsistent state. + txObject.getSessionHolder().getSession().clear(); + } + } + } + + @Override + protected void doSetRollbackOnly(DefaultTransactionStatus status) { + HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting Hibernate transaction on Session [" + + txObject.getSessionHolder().getSession() + "] rollback-only"); + } + txObject.setRollbackOnly(); + } + + @Override + protected void doCleanupAfterCompletion(Object transaction) { + HibernateTransactionObject txObject = (HibernateTransactionObject) transaction; + + // Remove the session holder from the thread. + if (txObject.isNewSessionHolder()) { + TransactionSynchronizationManager.unbindResource(getSessionFactory()); + } + + // Remove the JDBC connection holder from the thread, if exposed. + if (getDataSource() != null) { + TransactionSynchronizationManager.unbindResource(getDataSource()); + } + + Session session = txObject.getSessionHolder().getSession(); + if (this.prepareConnection && session.isConnected() && isSameConnectionForEntireSession(session)) { + // We're running with connection release mode "on_close": We're able to reset + // the isolation level and/or read-only flag of the JDBC Connection here. + // Else, we need to rely on the connection pool to perform proper cleanup. + try { + Connection con = ((SessionImplementor) session).connection(); + Integer previousHoldability = txObject.getPreviousHoldability(); + if (previousHoldability != null) { + con.setHoldability(previousHoldability); + } + DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel()); + } + catch (HibernateException ex) { + logger.debug("Could not access JDBC Connection of Hibernate Session", ex); + } + catch (Throwable ex) { + logger.debug("Could not reset JDBC Connection after transaction", ex); + } + } + + if (txObject.isNewSession()) { + if (logger.isDebugEnabled()) { + logger.debug("Closing Hibernate Session [" + session + "] after transaction"); + } + SessionFactoryUtils.closeSession(session); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Not closing pre-bound Hibernate Session [" + session + "] after transaction"); + } + if (txObject.getSessionHolder().getPreviousFlushMode() != null) { + session.setFlushMode(txObject.getSessionHolder().getPreviousFlushMode()); + } + if (!this.allowResultAccessAfterCompletion && !this.hibernateManagedSession) { + disconnectOnCompletion(session); + } + } + txObject.getSessionHolder().clear(); + } + + /** + * Disconnect a pre-existing Hibernate Session on transaction completion, + * returning its database connection but preserving its entity state. + *

The default implementation simply calls {@link Session#disconnect()}. + * Subclasses may override this with a no-op or with fine-tuned disconnection logic. + * @param session the Hibernate Session to disconnect + * @since 4.2 + * @see Session#disconnect() + */ + protected void disconnectOnCompletion(Session session) { + session.disconnect(); + } + + /** + * Return whether the given Hibernate Session will always hold the same + * JDBC Connection. This is used to check whether the transaction manager + * can safely prepare and clean up the JDBC Connection used for a transaction. + *

The default implementation checks the Session's connection release mode + * to be "on_close". + * @param session the Hibernate Session to check + * @see ConnectionReleaseMode#ON_CLOSE + */ + protected boolean isSameConnectionForEntireSession(Session session) { + // TODO: The best we can do is to assume we're safe. + return true; + } + + + /** + * Convert the given HibernateException to an appropriate exception + * from the {@code org.springframework.dao} hierarchy. + *

Will automatically apply a specified SQLExceptionTranslator to a + * Hibernate JDBCException, else rely on Hibernate's default translation. + * @param ex HibernateException that occurred + * @return a corresponding DataAccessException + * @see SessionFactoryUtils#convertHibernateAccessException + */ + protected DataAccessException convertHibernateAccessException(HibernateException ex) { + return SessionFactoryUtils.convertHibernateAccessException(ex); + } + + + /** + * Hibernate transaction object, representing a SessionHolder. + * Used as transaction object by HibernateTransactionManager. + */ + private class HibernateTransactionObject extends JdbcTransactionObjectSupport { + + private SessionHolder sessionHolder; + + private boolean newSessionHolder; + + private boolean newSession; + + private Integer previousHoldability; + + public void setSession(Session session) { + this.sessionHolder = new SessionHolder(session); + this.newSessionHolder = true; + this.newSession = true; + } + + public void setExistingSession(Session session) { + this.sessionHolder = new SessionHolder(session); + this.newSessionHolder = true; + this.newSession = false; + } + + public void setSessionHolder(SessionHolder sessionHolder) { + this.sessionHolder = sessionHolder; + this.newSessionHolder = false; + this.newSession = false; + } + + public SessionHolder getSessionHolder() { + return this.sessionHolder; + } + + public boolean isNewSessionHolder() { + return this.newSessionHolder; + } + + public boolean isNewSession() { + return this.newSession; + } + + public void setPreviousHoldability(Integer previousHoldability) { + this.previousHoldability = previousHoldability; + } + + public Integer getPreviousHoldability() { + return this.previousHoldability; + } + + public boolean hasSpringManagedTransaction() { + return (this.sessionHolder != null && this.sessionHolder.getTransaction() != null); + } + + public boolean hasHibernateManagedTransaction() { + return (this.sessionHolder != null && + this.sessionHolder.getSession().getTransaction().getStatus() == TransactionStatus.ACTIVE); + } + + public void setRollbackOnly() { + this.sessionHolder.setRollbackOnly(); + if (hasConnectionHolder()) { + getConnectionHolder().setRollbackOnly(); + } + } + + @Override + public boolean isRollbackOnly() { + return this.sessionHolder.isRollbackOnly() || + (hasConnectionHolder() && getConnectionHolder().isRollbackOnly()); + } + + @Override + public void flush() { + try { + this.sessionHolder.getSession().flush(); + } + catch (HibernateException ex) { + throw convertHibernateAccessException(ex); + } + } + } + + + /** + * Holder for suspended resources. + * Used internally by {@code doSuspend} and {@code doResume}. + */ + private static class SuspendedResourcesHolder { + + private final SessionHolder sessionHolder; + + private final ConnectionHolder connectionHolder; + + private SuspendedResourcesHolder(SessionHolder sessionHolder, ConnectionHolder conHolder) { + this.sessionHolder = sessionHolder; + this.connectionHolder = conHolder; + } + + private SessionHolder getSessionHolder() { + return this.sessionHolder; + } + + private ConnectionHolder getConnectionHolder() { + return this.connectionHolder; + } + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java new file mode 100644 index 0000000000..b7a8b18144 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java @@ -0,0 +1,454 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import java.io.File; +import java.io.IOException; +import java.util.Properties; +import javax.sql.DataSource; + +import org.hibernate.Interceptor; +import org.hibernate.SessionFactory; +import org.hibernate.boot.model.naming.ImplicitNamingStrategy; +import org.hibernate.boot.model.naming.PhysicalNamingStrategy; +import org.hibernate.cache.spi.RegionFactory; +import org.hibernate.cfg.Configuration; +import org.hibernate.context.spi.CurrentTenantIdentifierResolver; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.core.type.filter.TypeFilter; + +/** + * {@link FactoryBean} that creates a Hibernate + * {@link SessionFactory}. This is the usual way to set up a shared + * Hibernate SessionFactory in a Spring application context; the SessionFactory can + * then be passed to Hibernate-based data access objects via dependency injection. + * + *

This class is similar in role to the same-named class in the {@code orm.hibernate3} + * package. However, in practice, it is closer to {@code AnnotationSessionFactoryBean} + * since its core purpose is to bootstrap a {@code SessionFactory} from package scanning. + * + * @author Juergen Hoeller + * @since 4.2 + * @see #setDataSource + * @see #setPackagesToScan + * @see LocalSessionFactoryBuilder + */ +public class LocalSessionFactoryBean extends HibernateExceptionTranslator + implements FactoryBean, ResourceLoaderAware, InitializingBean, DisposableBean { + + private DataSource dataSource; + + private Resource[] configLocations; + + private String[] mappingResources; + + private Resource[] mappingLocations; + + private Resource[] cacheableMappingLocations; + + private Resource[] mappingJarLocations; + + private Resource[] mappingDirectoryLocations; + + private Interceptor entityInterceptor; + + private ImplicitNamingStrategy implicitNamingStrategy; + + private PhysicalNamingStrategy physicalNamingStrategy; + + private Object jtaTransactionManager; + + private CurrentTenantIdentifierResolver currentTenantIdentifierResolver; + + private TypeFilter[] entityTypeFilters; + + private Properties hibernateProperties; + + private Class[] annotatedClasses; + + private String[] annotatedPackages; + + private String[] packagesToScan; + + private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); + + private Configuration configuration; + + private SessionFactory sessionFactory; + + + /** + * Set the DataSource to be used by the SessionFactory. + * If set, this will override corresponding settings in Hibernate properties. + *

If this is set, the Hibernate settings should not define + * a connection provider to avoid meaningless double configuration. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Set the location of a single Hibernate XML config file, for example as + * classpath resource "classpath:hibernate.cfg.xml". + *

Note: Can be omitted when all necessary properties and mapping + * resources are specified locally via this bean. + * @see Configuration#configure(java.net.URL) + */ + public void setConfigLocation(Resource configLocation) { + this.configLocations = new Resource[] {configLocation}; + } + + /** + * Set the locations of multiple Hibernate XML config files, for example as + * classpath resources "classpath:hibernate.cfg.xml,classpath:extension.cfg.xml". + *

Note: Can be omitted when all necessary properties and mapping + * resources are specified locally via this bean. + * @see Configuration#configure(java.net.URL) + */ + public void setConfigLocations(Resource... configLocations) { + this.configLocations = configLocations; + } + + /** + * Set Hibernate mapping resources to be found in the class path, + * like "example.hbm.xml" or "mypackage/example.hbm.xml". + * Analogous to mapping entries in a Hibernate XML config file. + * Alternative to the more generic setMappingLocations method. + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see #setMappingLocations + * @see Configuration#addResource + */ + public void setMappingResources(String... mappingResources) { + this.mappingResources = mappingResources; + } + + /** + * Set locations of Hibernate mapping files, for example as classpath + * resource "classpath:example.hbm.xml". Supports any resource location + * via Spring's resource abstraction, for example relative paths like + * "WEB-INF/mappings/example.hbm.xml" when running in an application context. + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see Configuration#addInputStream + */ + public void setMappingLocations(Resource... mappingLocations) { + this.mappingLocations = mappingLocations; + } + + /** + * Set locations of cacheable Hibernate mapping files, for example as web app + * resource "/WEB-INF/mapping/example.hbm.xml". Supports any resource location + * via Spring's resource abstraction, as long as the resource can be resolved + * in the file system. + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see Configuration#addCacheableFile(File) + */ + public void setCacheableMappingLocations(Resource... cacheableMappingLocations) { + this.cacheableMappingLocations = cacheableMappingLocations; + } + + /** + * Set locations of jar files that contain Hibernate mapping resources, + * like "WEB-INF/lib/example.hbm.jar". + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see Configuration#addJar(File) + */ + public void setMappingJarLocations(Resource... mappingJarLocations) { + this.mappingJarLocations = mappingJarLocations; + } + + /** + * Set locations of directories that contain Hibernate mapping resources, + * like "WEB-INF/mappings". + *

Can be used to add to mappings from a Hibernate XML config file, + * or to specify all mappings locally. + * @see Configuration#addDirectory(File) + */ + public void setMappingDirectoryLocations(Resource... mappingDirectoryLocations) { + this.mappingDirectoryLocations = mappingDirectoryLocations; + } + + /** + * Set a Hibernate entity interceptor that allows to inspect and change + * property values before writing to and reading from the database. + * Will get applied to any new Session created by this factory. + * @see Configuration#setInterceptor + */ + public void setEntityInterceptor(Interceptor entityInterceptor) { + this.entityInterceptor = entityInterceptor; + } + + /** + * Set a Hibernate 5.0 ImplicitNamingStrategy for the SessionFactory. + * @see Configuration#setImplicitNamingStrategy + */ + public void setImplicitNamingStrategy(ImplicitNamingStrategy implicitNamingStrategy) { + this.implicitNamingStrategy = implicitNamingStrategy; + } + + /** + * Set a Hibernate 5.0 PhysicalNamingStrategy for the SessionFactory. + * @see Configuration#setPhysicalNamingStrategy + */ + public void setPhysicalNamingStrategy(PhysicalNamingStrategy physicalNamingStrategy) { + this.physicalNamingStrategy = physicalNamingStrategy; + } + + /** + * Set the Spring {@link org.springframework.transaction.jta.JtaTransactionManager} + * or the JTA {@link javax.transaction.TransactionManager} to be used with Hibernate, + * if any. Implicitly sets up {@code JtaPlatform}. + * @see LocalSessionFactoryBuilder#setJtaTransactionManager + */ + public void setJtaTransactionManager(Object jtaTransactionManager) { + this.jtaTransactionManager = jtaTransactionManager; + } + + /** + * Set a {@link CurrentTenantIdentifierResolver} to be passed on to the SessionFactory. + * @see LocalSessionFactoryBuilder#setCurrentTenantIdentifierResolver + */ + public void setCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver currentTenantIdentifierResolver) { + this.currentTenantIdentifierResolver = currentTenantIdentifierResolver; + } + + /** + * Specify custom type filters for Spring-based scanning for entity classes. + *

Default is to search all specified packages for classes annotated with + * {@code @javax.persistence.Entity}, {@code @javax.persistence.Embeddable} + * or {@code @javax.persistence.MappedSuperclass}. + * @since 4.2 + * @see #setPackagesToScan + */ + public void setEntityTypeFilters(TypeFilter... entityTypeFilters) { + this.entityTypeFilters = entityTypeFilters; + } + + /** + * Set Hibernate properties, such as "hibernate.dialect". + *

Note: Do not specify a transaction provider here when using + * Spring-driven transactions. It is also advisable to omit connection + * provider settings and use a Spring-set DataSource instead. + * @see #setDataSource + */ + public void setHibernateProperties(Properties hibernateProperties) { + this.hibernateProperties = hibernateProperties; + } + + /** + * Return the Hibernate properties, if any. Mainly available for + * configuration through property paths that specify individual keys. + */ + public Properties getHibernateProperties() { + if (this.hibernateProperties == null) { + this.hibernateProperties = new Properties(); + } + return this.hibernateProperties; + } + + /** + * Specify annotated entity classes to register with this Hibernate SessionFactory. + * @see Configuration#addAnnotatedClass(Class) + */ + public void setAnnotatedClasses(Class... annotatedClasses) { + this.annotatedClasses = annotatedClasses; + } + + /** + * Specify the names of annotated packages, for which package-level + * annotation metadata will be read. + * @see Configuration#addPackage(String) + */ + public void setAnnotatedPackages(String... annotatedPackages) { + this.annotatedPackages = annotatedPackages; + } + + /** + * Specify packages to search for autodetection of your entity classes in the + * classpath. This is analogous to Spring's component-scan feature + * ({@link org.springframework.context.annotation.ClassPathBeanDefinitionScanner}). + */ + public void setPackagesToScan(String... packagesToScan) { + this.packagesToScan = packagesToScan; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); + } + + + @Override + public void afterPropertiesSet() throws IOException { + LocalSessionFactoryBuilder sfb = new LocalSessionFactoryBuilder(this.dataSource, this.resourcePatternResolver); + + if (this.configLocations != null) { + for (Resource resource : this.configLocations) { + // Load Hibernate configuration from given location. + sfb.configure(resource.getURL()); + } + } + + if (this.mappingResources != null) { + // Register given Hibernate mapping definitions, contained in resource files. + for (String mapping : this.mappingResources) { + Resource mr = new ClassPathResource(mapping.trim(), this.resourcePatternResolver.getClassLoader()); + sfb.addInputStream(mr.getInputStream()); + } + } + + if (this.mappingLocations != null) { + // Register given Hibernate mapping definitions, contained in resource files. + for (Resource resource : this.mappingLocations) { + sfb.addInputStream(resource.getInputStream()); + } + } + + if (this.cacheableMappingLocations != null) { + // Register given cacheable Hibernate mapping definitions, read from the file system. + for (Resource resource : this.cacheableMappingLocations) { + sfb.addCacheableFile(resource.getFile()); + } + } + + if (this.mappingJarLocations != null) { + // Register given Hibernate mapping definitions, contained in jar files. + for (Resource resource : this.mappingJarLocations) { + sfb.addJar(resource.getFile()); + } + } + + if (this.mappingDirectoryLocations != null) { + // Register all Hibernate mapping definitions in the given directories. + for (Resource resource : this.mappingDirectoryLocations) { + File file = resource.getFile(); + if (!file.isDirectory()) { + throw new IllegalArgumentException( + "Mapping directory location [" + resource + "] does not denote a directory"); + } + sfb.addDirectory(file); + } + } + + if (this.entityInterceptor != null) { + sfb.setInterceptor(this.entityInterceptor); + } + + if (this.implicitNamingStrategy != null) { + sfb.setImplicitNamingStrategy(this.implicitNamingStrategy); + } + + if (this.physicalNamingStrategy != null) { + sfb.setPhysicalNamingStrategy(this.physicalNamingStrategy); + } + + if (this.jtaTransactionManager != null) { + sfb.setJtaTransactionManager(this.jtaTransactionManager); + } + + if (this.currentTenantIdentifierResolver != null) { + sfb.setCurrentTenantIdentifierResolver(this.currentTenantIdentifierResolver); + } + + if (this.entityTypeFilters != null) { + sfb.setEntityTypeFilters(this.entityTypeFilters); + } + + if (this.hibernateProperties != null) { + sfb.addProperties(this.hibernateProperties); + } + + if (this.annotatedClasses != null) { + sfb.addAnnotatedClasses(this.annotatedClasses); + } + + if (this.annotatedPackages != null) { + sfb.addPackages(this.annotatedPackages); + } + + if (this.packagesToScan != null) { + sfb.scanPackages(this.packagesToScan); + } + + // Build SessionFactory instance. + this.configuration = sfb; + this.sessionFactory = buildSessionFactory(sfb); + } + + /** + * Subclasses can override this method to perform custom initialization + * of the SessionFactory instance, creating it via the given Configuration + * object that got prepared by this LocalSessionFactoryBean. + *

The default implementation invokes LocalSessionFactoryBuilder's buildSessionFactory. + * A custom implementation could prepare the instance in a specific way (e.g. applying + * a custom ServiceRegistry) or use a custom SessionFactoryImpl subclass. + * @param sfb LocalSessionFactoryBuilder prepared by this LocalSessionFactoryBean + * @return the SessionFactory instance + * @see LocalSessionFactoryBuilder#buildSessionFactory + */ + protected SessionFactory buildSessionFactory(LocalSessionFactoryBuilder sfb) { + return sfb.buildSessionFactory(); + } + + /** + * Return the Hibernate Configuration object used to build the SessionFactory. + * Allows for access to configuration metadata stored there (rarely needed). + * @throws IllegalStateException if the Configuration object has not been initialized yet + */ + public final Configuration getConfiguration() { + if (this.configuration == null) { + throw new IllegalStateException("Configuration not initialized yet"); + } + return this.configuration; + } + + + @Override + public SessionFactory getObject() { + return this.sessionFactory; + } + + @Override + public Class getObjectType() { + return (this.sessionFactory != null ? this.sessionFactory.getClass() : SessionFactory.class); + } + + @Override + public boolean isSingleton() { + return true; + } + + + @Override + public void destroy() { + this.sessionFactory.close(); + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java new file mode 100644 index 0000000000..5268b1212c --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java @@ -0,0 +1,266 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import java.io.IOException; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.MappedSuperclass; +import javax.sql.DataSource; +import javax.transaction.TransactionManager; + +import org.hibernate.HibernateException; +import org.hibernate.MappingException; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.cfg.Environment; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.transaction.jta.JtaTransactionManager; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * A Spring-provided extension of the standard Hibernate {@link Configuration} class, + * adding {@link SpringSessionContext} as a default and providing convenient ways + * to specify a DataSource and an application class loader. + * + *

This is designed for programmatic use, e.g. in {@code @Bean} factory methods. + * Consider using {@link LocalSessionFactoryBean} for XML bean definition files. + * + * @author Juergen Hoeller + * @since 4.2 + * @see LocalSessionFactoryBean + */ +@SuppressWarnings("serial") +public class LocalSessionFactoryBuilder extends Configuration { + + private static final String RESOURCE_PATTERN = "/**/*.class"; + + private static final String PACKAGE_INFO_SUFFIX = ".package-info"; + + private static final TypeFilter[] DEFAULT_ENTITY_TYPE_FILTERS = new TypeFilter[] { + new AnnotationTypeFilter(Entity.class, false), + new AnnotationTypeFilter(Embeddable.class, false), + new AnnotationTypeFilter(MappedSuperclass.class, false)}; + + private final TypeFilter CONVERTER_TYPE_FILTER = new AnnotationTypeFilter(Converter.class, false); + + + private final ResourcePatternResolver resourcePatternResolver; + + private TypeFilter[] entityTypeFilters = DEFAULT_ENTITY_TYPE_FILTERS; + + + /** + * Create a new LocalSessionFactoryBuilder for the given DataSource. + * @param dataSource the JDBC DataSource that the resulting Hibernate SessionFactory should be using + * (may be {@code null}) + */ + public LocalSessionFactoryBuilder(DataSource dataSource) { + this(dataSource, new PathMatchingResourcePatternResolver()); + } + + /** + * Create a new LocalSessionFactoryBuilder for the given DataSource. + * @param dataSource the JDBC DataSource that the resulting Hibernate SessionFactory should be using + * (may be {@code null}) + * @param classLoader the ClassLoader to load application classes from + */ + public LocalSessionFactoryBuilder(DataSource dataSource, ClassLoader classLoader) { + this(dataSource, new PathMatchingResourcePatternResolver(classLoader)); + } + + /** + * Create a new LocalSessionFactoryBuilder for the given DataSource. + * @param dataSource the JDBC DataSource that the resulting Hibernate SessionFactory should be using + * (may be {@code null}) + * @param resourceLoader the ResourceLoader to load application classes from + */ + public LocalSessionFactoryBuilder(DataSource dataSource, ResourceLoader resourceLoader) { + getProperties().put(Environment.CURRENT_SESSION_CONTEXT_CLASS, SpringSessionContext.class.getName()); + if (dataSource != null) { + getProperties().put(Environment.DATASOURCE, dataSource); + } + getProperties().put(AvailableSettings.CLASSLOADERS, Collections.singleton(resourceLoader.getClassLoader())); + this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); + } + + + /** + * Set the Spring {@link JtaTransactionManager} or the JTA {@link TransactionManager} + * to be used with Hibernate, if any. Allows for using a Spring-managed transaction + * manager for Hibernate 4's session and cache synchronization, with the + * "hibernate.transaction.jta.platform" automatically set to it. + *

A passed-in Spring {@link JtaTransactionManager} needs to contain a JTA + * {@link TransactionManager} reference to be usable here, except for the WebSphere + * case where we'll automatically set {@code WebSphereExtendedJtaPlatform} accordingly. + *

Note: If this is set, the Hibernate settings should not contain a JTA platform + * setting to avoid meaningless double configuration. + */ + public LocalSessionFactoryBuilder setJtaTransactionManager(Object jtaTransactionManager) { + Assert.notNull(jtaTransactionManager, "Transaction manager reference must not be null"); + if (jtaTransactionManager instanceof JtaTransactionManager) { + boolean webspherePresent = ClassUtils.isPresent("com.ibm.wsspi.uow.UOWManager", getClass().getClassLoader()); + if (webspherePresent) { + getProperties().put(AvailableSettings.JTA_PLATFORM, + "org.hibernate.engine.transaction.jta.platform.internal.WebSphereExtendedJtaPlatform"); + } + else { + JtaTransactionManager jtaTm = (JtaTransactionManager) jtaTransactionManager; + if (jtaTm.getTransactionManager() == null) { + throw new IllegalArgumentException( + "Can only apply JtaTransactionManager which has a TransactionManager reference set"); + } + getProperties().put(AvailableSettings.JTA_PLATFORM, + new ConfigurableJtaPlatform(jtaTm.getTransactionManager(), jtaTm.getUserTransaction(), + jtaTm.getTransactionSynchronizationRegistry())); + } + } + else if (jtaTransactionManager instanceof TransactionManager) { + getProperties().put(AvailableSettings.JTA_PLATFORM, + new ConfigurableJtaPlatform((TransactionManager) jtaTransactionManager, null, null)); + } + else { + throw new IllegalArgumentException( + "Unknown transaction manager type: " + jtaTransactionManager.getClass().getName()); + } + return this; + } + + /** + * Specify custom type filters for Spring-based scanning for entity classes. + *

Default is to search all specified packages for classes annotated with + * {@code @javax.persistence.Entity}, {@code @javax.persistence.Embeddable} + * or {@code @javax.persistence.MappedSuperclass}. + * @since 4.2 + * @see #scanPackages + */ + public LocalSessionFactoryBuilder setEntityTypeFilters(TypeFilter... entityTypeFilters) { + this.entityTypeFilters = entityTypeFilters; + return this; + } + + /** + * Add the given annotated classes in a batch. + * @see #addAnnotatedClass + * @see #scanPackages + */ + public LocalSessionFactoryBuilder addAnnotatedClasses(Class... annotatedClasses) { + for (Class annotatedClass : annotatedClasses) { + addAnnotatedClass(annotatedClass); + } + return this; + } + + /** + * Add the given annotated packages in a batch. + * @see #addPackage + * @see #scanPackages + */ + public LocalSessionFactoryBuilder addPackages(String... annotatedPackages) { + for (String annotatedPackage : annotatedPackages) { + addPackage(annotatedPackage); + } + return this; + } + + /** + * Perform Spring-based scanning for entity classes, registering them + * as annotated classes with this {@code Configuration}. + * @param packagesToScan one or more Java package names + * @throws HibernateException if scanning fails for any reason + */ + @SuppressWarnings("unchecked") + public LocalSessionFactoryBuilder scanPackages(String... packagesToScan) throws HibernateException { + Set entityClassNames = new TreeSet(); + Set converterClassNames = new TreeSet(); + Set packageNames = new TreeSet(); + try { + for (String pkg : packagesToScan) { + String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(pkg) + RESOURCE_PATTERN; + Resource[] resources = this.resourcePatternResolver.getResources(pattern); + MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver); + for (Resource resource : resources) { + if (resource.isReadable()) { + MetadataReader reader = readerFactory.getMetadataReader(resource); + String className = reader.getClassMetadata().getClassName(); + if (matchesEntityTypeFilter(reader, readerFactory)) { + entityClassNames.add(className); + } + else if (CONVERTER_TYPE_FILTER.match(reader, readerFactory)) { + converterClassNames.add(className); + } + else if (className.endsWith(PACKAGE_INFO_SUFFIX)) { + packageNames.add(className.substring(0, className.length() - PACKAGE_INFO_SUFFIX.length())); + } + } + } + } + } + catch (IOException ex) { + throw new MappingException("Failed to scan classpath for unlisted classes", ex); + } + try { + ClassLoader cl = this.resourcePatternResolver.getClassLoader(); + for (String className : entityClassNames) { + addAnnotatedClass(cl.loadClass(className)); + } + for (String className : converterClassNames) { + addAttributeConverter((Class>) cl.loadClass(className)); + } + for (String packageName : packageNames) { + addPackage(packageName); + } + } + catch (ClassNotFoundException ex) { + throw new MappingException("Failed to load annotated classes from classpath", ex); + } + return this; + } + + /** + * Check whether any of the configured entity type filters matches + * the current class descriptor contained in the metadata reader. + */ + private boolean matchesEntityTypeFilter(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException { + if (this.entityTypeFilters != null) { + for (TypeFilter filter : this.entityTypeFilters) { + if (filter.match(reader, readerFactory)) { + return true; + } + } + } + return false; + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java new file mode 100644 index 0000000000..b852371dbc --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java @@ -0,0 +1,213 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.HibernateException; +import org.hibernate.JDBCException; +import org.hibernate.NonUniqueObjectException; +import org.hibernate.NonUniqueResultException; +import org.hibernate.ObjectDeletedException; +import org.hibernate.PersistentObjectException; +import org.hibernate.PessimisticLockException; +import org.hibernate.PropertyValueException; +import org.hibernate.QueryException; +import org.hibernate.QueryTimeoutException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.StaleObjectStateException; +import org.hibernate.StaleStateException; +import org.hibernate.TransientObjectException; +import org.hibernate.UnresolvableObjectException; +import org.hibernate.WrongClassException; +import org.hibernate.dialect.lock.OptimisticEntityLockException; +import org.hibernate.dialect.lock.PessimisticEntityLockException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.DataException; +import org.hibernate.exception.JDBCConnectionException; +import org.hibernate.exception.LockAcquisitionException; +import org.hibernate.exception.SQLGrammarException; +import org.hibernate.service.spi.Wrapped; + +import org.springframework.dao.CannotAcquireLockException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.dao.PessimisticLockingFailureException; +import org.springframework.jdbc.datasource.DataSourceUtils; + +/** + * Helper class featuring methods for Hibernate Session handling. + * Also provides support for exception translation. + * + *

Used internally by {@link HibernateTransactionManager}. + * Can also be used directly in application code. + * + * @author Juergen Hoeller + * @since 4.2 + * @see HibernateExceptionTranslator + * @see HibernateTransactionManager + */ +public abstract class SessionFactoryUtils { + + /** + * Order value for TransactionSynchronization objects that clean up Hibernate Sessions. + * Returns {@code DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 100} + * to execute Session cleanup before JDBC Connection cleanup, if any. + * @see DataSourceUtils#CONNECTION_SYNCHRONIZATION_ORDER + */ + public static final int SESSION_SYNCHRONIZATION_ORDER = + DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 100; + + static final Log logger = LogFactory.getLog(SessionFactoryUtils.class); + + + /** + * Determine the DataSource of the given SessionFactory. + * @param sessionFactory the SessionFactory to check + * @return the DataSource, or {@code null} if none found + * @see SessionFactoryImplementor#getConnectionProvider + */ + @SuppressWarnings("deprecation") + public static DataSource getDataSource(SessionFactory sessionFactory) { + if (sessionFactory instanceof SessionFactoryImplementor) { + Wrapped cp = ((SessionFactoryImplementor) sessionFactory).getConnectionProvider(); + if (cp != null) { + return cp.unwrap(DataSource.class); + } + } + return null; + } + + /** + * Perform actual closing of the Hibernate Session, + * catching and logging any cleanup exceptions thrown. + * @param session the Hibernate Session to close (may be {@code null}) + * @see Session#close() + */ + public static void closeSession(Session session) { + if (session != null) { + try { + session.close(); + } + catch (HibernateException ex) { + logger.debug("Could not close Hibernate Session", ex); + } + catch (Throwable ex) { + logger.debug("Unexpected exception on closing Hibernate Session", ex); + } + } + } + + /** + * Convert the given HibernateException to an appropriate exception + * from the {@code org.springframework.dao} hierarchy. + * @param ex HibernateException that occurred + * @return the corresponding DataAccessException instance + * @see HibernateExceptionTranslator#convertHibernateAccessException + * @see HibernateTransactionManager#convertHibernateAccessException + */ + public static DataAccessException convertHibernateAccessException(HibernateException ex) { + if (ex instanceof JDBCConnectionException) { + return new DataAccessResourceFailureException(ex.getMessage(), ex); + } + if (ex instanceof SQLGrammarException) { + SQLGrammarException jdbcEx = (SQLGrammarException) ex; + return new InvalidDataAccessResourceUsageException(ex.getMessage() + "; SQL [" + jdbcEx.getSQL() + "]", ex); + } + if (ex instanceof QueryTimeoutException) { + QueryTimeoutException jdbcEx = (QueryTimeoutException) ex; + return new org.springframework.dao.QueryTimeoutException(ex.getMessage() + "; SQL [" + jdbcEx.getSQL() + "]", ex); + } + if (ex instanceof LockAcquisitionException) { + LockAcquisitionException jdbcEx = (LockAcquisitionException) ex; + return new CannotAcquireLockException(ex.getMessage() + "; SQL [" + jdbcEx.getSQL() + "]", ex); + } + if (ex instanceof PessimisticLockException) { + PessimisticLockException jdbcEx = (PessimisticLockException) ex; + return new PessimisticLockingFailureException(ex.getMessage() + "; SQL [" + jdbcEx.getSQL() + "]", ex); + } + if (ex instanceof ConstraintViolationException) { + ConstraintViolationException jdbcEx = (ConstraintViolationException) ex; + return new DataIntegrityViolationException(ex.getMessage() + "; SQL [" + jdbcEx.getSQL() + + "]; constraint [" + jdbcEx.getConstraintName() + "]", ex); + } + if (ex instanceof DataException) { + DataException jdbcEx = (DataException) ex; + return new DataIntegrityViolationException(ex.getMessage() + "; SQL [" + jdbcEx.getSQL() + "]", ex); + } + if (ex instanceof JDBCException) { + return new HibernateJdbcException((JDBCException) ex); + } + // end of JDBCException (subclass) handling + + if (ex instanceof QueryException) { + return new HibernateQueryException((QueryException) ex); + } + if (ex instanceof NonUniqueResultException) { + return new IncorrectResultSizeDataAccessException(ex.getMessage(), 1, ex); + } + if (ex instanceof NonUniqueObjectException) { + return new DuplicateKeyException(ex.getMessage(), ex); + } + if (ex instanceof PropertyValueException) { + return new DataIntegrityViolationException(ex.getMessage(), ex); + } + if (ex instanceof PersistentObjectException) { + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } + if (ex instanceof TransientObjectException) { + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } + if (ex instanceof ObjectDeletedException) { + return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); + } + if (ex instanceof UnresolvableObjectException) { + return new HibernateObjectRetrievalFailureException((UnresolvableObjectException) ex); + } + if (ex instanceof WrongClassException) { + return new HibernateObjectRetrievalFailureException((WrongClassException) ex); + } + if (ex instanceof StaleObjectStateException) { + return new HibernateOptimisticLockingFailureException((StaleObjectStateException) ex); + } + if (ex instanceof StaleStateException) { + return new HibernateOptimisticLockingFailureException((StaleStateException) ex); + } + if (ex instanceof OptimisticEntityLockException) { + return new HibernateOptimisticLockingFailureException((OptimisticEntityLockException) ex); + } + if (ex instanceof PessimisticEntityLockException) { + if (ex.getCause() instanceof LockAcquisitionException) { + return new CannotAcquireLockException(ex.getMessage(), ex.getCause()); + } + return new PessimisticLockingFailureException(ex.getMessage(), ex); + } + + // fallback + return new HibernateSystemException(ex); + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SessionHolder.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SessionHolder.java new file mode 100644 index 0000000000..53866fedb9 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SessionHolder.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import org.hibernate.FlushMode; +import org.hibernate.Session; +import org.hibernate.Transaction; + +import org.springframework.transaction.support.ResourceHolderSupport; +import org.springframework.util.Assert; + +/** + * Session holder, wrapping a Hibernate Session and a Hibernate Transaction. + * HibernateTransactionManager binds instances of this class to the thread, + * for a given SessionFactory. + * + *

Note: This is an SPI class, not intended to be used by applications. + * + * @author Juergen Hoeller + * @since 4.2 + * @see HibernateTransactionManager + * @see SessionFactoryUtils + */ +public class SessionHolder extends ResourceHolderSupport { + + private Session session; + + private Transaction transaction; + + private FlushMode previousFlushMode; + + + public SessionHolder(Session session) { + Assert.notNull(session, "Session must not be null"); + this.session = session; + } + + public Session getSession() { + return this.session; + } + + public void setTransaction(Transaction transaction) { + this.transaction = transaction; + } + + public Transaction getTransaction() { + return this.transaction; + } + + public void setPreviousFlushMode(FlushMode previousFlushMode) { + this.previousFlushMode = previousFlushMode; + } + + public FlushMode getPreviousFlushMode() { + return this.previousFlushMode; + } + + + @Override + public void clear() { + super.clear(); + this.transaction = null; + this.previousFlushMode = null; + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java new file mode 100644 index 0000000000..b421a4f374 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import org.hibernate.HibernateException; +import org.hibernate.Session; + +import org.springframework.transaction.support.TransactionSynchronizationAdapter; + +/** + * Simple synchronization adapter that propagates a {@code flush()} call + * to the underlying Hibernate Session. Used in combination with JTA. + * + * @author Juergen Hoeller + * @since 4.2 + */ +public class SpringFlushSynchronization extends TransactionSynchronizationAdapter { + + private final Session session; + + + public SpringFlushSynchronization(Session session) { + this.session = session; + } + + + @Override + public void flush() { + try { + SessionFactoryUtils.logger.debug("Flushing Hibernate Session on explicit request"); + this.session.flush(); + } + catch (HibernateException ex) { + throw SessionFactoryUtils.convertHibernateAccessException(ex); + } + } + + + @Override + public boolean equals(Object obj) { + return (obj instanceof SpringFlushSynchronization && + this.session == ((SpringFlushSynchronization) obj).session); + } + + @Override + public int hashCode() { + return this.session.hashCode(); + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SpringJtaSessionContext.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SpringJtaSessionContext.java new file mode 100644 index 0000000000..a4eb34ed40 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SpringJtaSessionContext.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import org.hibernate.FlushMode; +import org.hibernate.Session; +import org.hibernate.context.internal.JTASessionContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; + +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Spring-specific subclass of Hibernate's JTASessionContext, + * setting {@code FlushMode.MANUAL} for read-only transactions. + * + * @author Juergen Hoeller + * @since 4.2 + */ +@SuppressWarnings("serial") +public class SpringJtaSessionContext extends JTASessionContext { + + public SpringJtaSessionContext(SessionFactoryImplementor factory) { + super(factory); + } + + @Override + protected Session buildOrObtainSession() { + Session session = super.buildOrObtainSession(); + if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + session.setFlushMode(FlushMode.MANUAL); + } + return session; + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SpringSessionContext.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SpringSessionContext.java new file mode 100644 index 0000000000..2ad96a7901 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SpringSessionContext.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.TransactionManager; + +import org.apache.commons.logging.LogFactory; +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.context.spi.CurrentSessionContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; + +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Implementation of Hibernate 3.1's CurrentSessionContext interface + * that delegates to Spring's SessionFactoryUtils for providing a + * Spring-managed current Session. + * + *

This CurrentSessionContext implementation can also be specified in custom + * SessionFactory setup through the "hibernate.current_session_context_class" + * property, with the fully qualified name of this class as value. + * + * @author Juergen Hoeller + * @since 4.2 + */ +@SuppressWarnings("serial") +public class SpringSessionContext implements CurrentSessionContext { + + private final SessionFactoryImplementor sessionFactory; + + private TransactionManager transactionManager; + + private CurrentSessionContext jtaSessionContext; + + + /** + * Create a new SpringSessionContext for the given Hibernate SessionFactory. + * @param sessionFactory the SessionFactory to provide current Sessions for + */ + public SpringSessionContext(SessionFactoryImplementor sessionFactory) { + this.sessionFactory = sessionFactory; + try { + JtaPlatform jtaPlatform = sessionFactory.getServiceRegistry().getService(JtaPlatform.class); + this.transactionManager = jtaPlatform.retrieveTransactionManager(); + if (this.transactionManager != null) { + this.jtaSessionContext = new SpringJtaSessionContext(sessionFactory); + } + } + catch (Exception ex) { + LogFactory.getLog(SpringSessionContext.class).warn( + "Could not introspect Hibernate JtaPlatform for SpringJtaSessionContext", ex); + } + } + + + /** + * Retrieve the Spring-managed Session for the current thread, if any. + */ + @Override + public Session currentSession() throws HibernateException { + Object value = TransactionSynchronizationManager.getResource(this.sessionFactory); + if (value instanceof Session) { + return (Session) value; + } + else if (value instanceof SessionHolder) { + SessionHolder sessionHolder = (SessionHolder) value; + Session session = sessionHolder.getSession(); + if (!sessionHolder.isSynchronizedWithTransaction() && + TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization( + new SpringSessionSynchronization(sessionHolder, this.sessionFactory, false)); + sessionHolder.setSynchronizedWithTransaction(true); + // Switch to FlushMode.AUTO, as we have to assume a thread-bound Session + // with FlushMode.MANUAL, which needs to allow flushing within the transaction. + FlushMode flushMode = session.getFlushMode(); + if (flushMode.equals(FlushMode.MANUAL) && + !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + session.setFlushMode(FlushMode.AUTO); + sessionHolder.setPreviousFlushMode(flushMode); + } + } + return session; + } + + if (this.transactionManager != null) { + try { + if (this.transactionManager.getStatus() == Status.STATUS_ACTIVE) { + Session session = this.jtaSessionContext.currentSession(); + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new SpringFlushSynchronization(session)); + } + return session; + } + } + catch (SystemException ex) { + throw new HibernateException("JTA TransactionManager found but status check failed", ex); + } + } + + if (TransactionSynchronizationManager.isSynchronizationActive()) { + Session session = this.sessionFactory.openSession(); + if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + session.setFlushMode(FlushMode.MANUAL); + } + SessionHolder sessionHolder = new SessionHolder(session); + TransactionSynchronizationManager.registerSynchronization( + new SpringSessionSynchronization(sessionHolder, this.sessionFactory, true)); + TransactionSynchronizationManager.bindResource(this.sessionFactory, sessionHolder); + sessionHolder.setSynchronizedWithTransaction(true); + return session; + } + else { + throw new HibernateException("Could not obtain transaction-synchronized Session for current thread"); + } + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SpringSessionSynchronization.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SpringSessionSynchronization.java new file mode 100644 index 0000000000..8cea09d490 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/SpringSessionSynchronization.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5; + +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import org.springframework.core.Ordered; +import org.springframework.dao.DataAccessException; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Callback for resource cleanup at the end of a Spring-managed transaction + * for a pre-bound Hibernate Session. + * + * @author Juergen Hoeller + * @since 4.2 + */ +public class SpringSessionSynchronization implements TransactionSynchronization, Ordered { + + private final SessionHolder sessionHolder; + + private final SessionFactory sessionFactory; + + private final boolean newSession; + + private boolean holderActive = true; + + + public SpringSessionSynchronization(SessionHolder sessionHolder, SessionFactory sessionFactory) { + this(sessionHolder, sessionFactory, false); + } + + public SpringSessionSynchronization(SessionHolder sessionHolder, SessionFactory sessionFactory, boolean newSession) { + this.sessionHolder = sessionHolder; + this.sessionFactory = sessionFactory; + this.newSession = newSession; + } + + + private Session getCurrentSession() { + return this.sessionHolder.getSession(); + } + + + @Override + public int getOrder() { + return SessionFactoryUtils.SESSION_SYNCHRONIZATION_ORDER; + } + + @Override + public void suspend() { + if (this.holderActive) { + TransactionSynchronizationManager.unbindResource(this.sessionFactory); + // Eagerly disconnect the Session here, to make release mode "on_close" work on JBoss. + getCurrentSession().disconnect(); + } + } + + @Override + public void resume() { + if (this.holderActive) { + TransactionSynchronizationManager.bindResource(this.sessionFactory, this.sessionHolder); + } + } + + @Override + public void flush() { + try { + SessionFactoryUtils.logger.debug("Flushing Hibernate Session on explicit request"); + getCurrentSession().flush(); + } + catch (HibernateException ex) { + throw SessionFactoryUtils.convertHibernateAccessException(ex); + } + } + + @Override + public void beforeCommit(boolean readOnly) throws DataAccessException { + if (!readOnly) { + Session session = getCurrentSession(); + // Read-write transaction -> flush the Hibernate Session. + // Further check: only flush when not FlushMode.MANUAL. + if (!session.getFlushMode().equals(FlushMode.MANUAL)) { + try { + SessionFactoryUtils.logger.debug("Flushing Hibernate Session on transaction synchronization"); + session.flush(); + } + catch (HibernateException ex) { + throw SessionFactoryUtils.convertHibernateAccessException(ex); + } + } + } + } + + @Override + public void beforeCompletion() { + Session session = this.sessionHolder.getSession(); + if (this.sessionHolder.getPreviousFlushMode() != null) { + // In case of pre-bound Session, restore previous flush mode. + session.setFlushMode(this.sessionHolder.getPreviousFlushMode()); + } + // Eagerly disconnect the Session here, to make release mode "on_close" work nicely. + session.disconnect(); + + // Unbind at this point if it's a new Session... + if (this.newSession) { + TransactionSynchronizationManager.unbindResource(this.sessionFactory); + this.holderActive = false; + } + } + + @Override + public void afterCommit() { + } + + @Override + public void afterCompletion(int status) { + try { + if (status != STATUS_COMMITTED) { + // Clear all pending inserts/updates/deletes in the Session. + // Necessary for pre-bound Sessions, to avoid inconsistent state. + this.sessionHolder.getSession().clear(); + } + } + finally { + this.sessionHolder.setSynchronizedWithTransaction(false); + // Call close() at this point if it's a new Session... + if (this.newSession) { + SessionFactoryUtils.closeSession(this.sessionHolder.getSession()); + } + } + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/package-info.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/package-info.java new file mode 100644 index 0000000000..16d9f6a8c5 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/package-info.java @@ -0,0 +1,16 @@ +/** + * Package providing integration of + * Hibernate 4.x + * with Spring concepts. + * + *

Contains an implementation of Spring's transaction SPI for local Hibernate transactions. + * This package is intentionally rather minimal, with no template classes or the like, + * in order to follow Hibernate recommendations as closely as possible. We recommend + * using Hibernate's native sessionFactory.getCurrentSession() style. + * + *

This package supports Hibernate 4.x only. + * See the {@code org.springframework.orm.hibernate3} package for Hibernate 3.x support. + * Note: Do not use HibernateTemplate or other classes from the hibernate3 package + * with Hibernate 4; this will lead to class definition exceptions at runtime. + */ +package org.springframework.orm.hibernate5; diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/AsyncRequestInterceptor.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/AsyncRequestInterceptor.java new file mode 100644 index 0000000000..dfc31c1b29 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/AsyncRequestInterceptor.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5.support; + +import java.util.concurrent.Callable; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.SessionFactory; + +import org.springframework.orm.hibernate5.SessionFactoryUtils; +import org.springframework.orm.hibernate5.SessionHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor; + +/** + * An interceptor with asynchronous web requests used in OpenSessionInViewFilter and + * OpenSessionInViewInterceptor. + * + * Ensures the following: + * 1) The session is bound/unbound when "callable processing" is started + * 2) The session is closed if an async request times out + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +class AsyncRequestInterceptor extends CallableProcessingInterceptorAdapter implements DeferredResultProcessingInterceptor { + + private static final Log logger = LogFactory.getLog(AsyncRequestInterceptor.class); + + private final SessionFactory sessionFactory; + + private final SessionHolder sessionHolder; + + private volatile boolean timeoutInProgress; + + + public AsyncRequestInterceptor(SessionFactory sessionFactory, SessionHolder sessionHolder) { + this.sessionFactory = sessionFactory; + this.sessionHolder = sessionHolder; + } + + + @Override + public void preProcess(NativeWebRequest request, Callable task) { + bindSession(); + } + + public void bindSession() { + this.timeoutInProgress = false; + TransactionSynchronizationManager.bindResource(this.sessionFactory, this.sessionHolder); + } + + @Override + public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) { + TransactionSynchronizationManager.unbindResource(this.sessionFactory); + } + + @Override + public Object handleTimeout(NativeWebRequest request, Callable task) { + this.timeoutInProgress = true; + return RESULT_NONE; // give other interceptors a chance to handle the timeout + } + + @Override + public void afterCompletion(NativeWebRequest request, Callable task) throws Exception { + closeAfterTimeout(); + } + + private void closeAfterTimeout() { + if (this.timeoutInProgress) { + logger.debug("Closing Hibernate Session after async request timeout"); + SessionFactoryUtils.closeSession(this.sessionHolder.getSession()); + } + } + + + // Implementation of DeferredResultProcessingInterceptor methods + + @Override + public void beforeConcurrentHandling(NativeWebRequest request, DeferredResult deferredResult) { + } + + @Override + public void preProcess(NativeWebRequest request, DeferredResult deferredResult) { + } + + @Override + public void postProcess(NativeWebRequest request, DeferredResult deferredResult, Object result) { + } + + @Override + public boolean handleTimeout(NativeWebRequest request, DeferredResult deferredResult) { + this.timeoutInProgress = true; + return true; // give other interceptors a chance to handle the timeout + } + + @Override + public void afterCompletion(NativeWebRequest request, DeferredResult deferredResult) { + closeAfterTimeout(); + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/HibernateDaoSupport.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/HibernateDaoSupport.java new file mode 100644 index 0000000000..8a86f29c45 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/HibernateDaoSupport.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5.support; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.support.DaoSupport; +import org.springframework.orm.hibernate5.HibernateTemplate; + +/** + * Convenient super class for Hibernate-based data access objects. + * + *

Requires a {@link SessionFactory} to be set, providing a + * {@link org.springframework.orm.hibernate5.HibernateTemplate} based on it to + * subclasses through the {@link #getHibernateTemplate()} method. + * Can alternatively be initialized directly with a HibernateTemplate, + * in order to reuse the latter's settings such as the SessionFactory, + * exception translator, flush mode, etc. + * + *

This class will create its own HibernateTemplate instance if a SessionFactory + * is passed in. The "allowCreate" flag on that HibernateTemplate will be "true" + * by default. A custom HibernateTemplate instance can be used through overriding + * {@link #createHibernateTemplate}. + * + *

NOTE: Hibernate access code can also be coded in plain Hibernate style. + * Hence, for newly started projects, consider adopting the standard Hibernate + * style of coding data access objects instead, based on + * {@link SessionFactory#getCurrentSession()}. + * This HibernateTemplate primarily exists as a migration helper for Hibernate 3 + * based data access code, to benefit from bug fixes in Hibernate 4.x. + * + * @author Juergen Hoeller + * @since 4.2 + * @see #setSessionFactory + * @see #getHibernateTemplate + * @see org.springframework.orm.hibernate5.HibernateTemplate + */ +public abstract class HibernateDaoSupport extends DaoSupport { + + private HibernateTemplate hibernateTemplate; + + + /** + * Set the Hibernate SessionFactory to be used by this DAO. + * Will automatically create a HibernateTemplate for the given SessionFactory. + * @see #createHibernateTemplate + * @see #setHibernateTemplate + */ + public final void setSessionFactory(SessionFactory sessionFactory) { + if (this.hibernateTemplate == null || sessionFactory != this.hibernateTemplate.getSessionFactory()) { + this.hibernateTemplate = createHibernateTemplate(sessionFactory); + } + } + + /** + * Create a HibernateTemplate for the given SessionFactory. + * Only invoked if populating the DAO with a SessionFactory reference! + *

Can be overridden in subclasses to provide a HibernateTemplate instance + * with different configuration, or a custom HibernateTemplate subclass. + * @param sessionFactory the Hibernate SessionFactory to create a HibernateTemplate for + * @return the new HibernateTemplate instance + * @see #setSessionFactory + */ + protected HibernateTemplate createHibernateTemplate(SessionFactory sessionFactory) { + return new HibernateTemplate(sessionFactory); + } + + /** + * Return the Hibernate SessionFactory used by this DAO. + */ + public final SessionFactory getSessionFactory() { + return (this.hibernateTemplate != null ? this.hibernateTemplate.getSessionFactory() : null); + } + + /** + * Set the HibernateTemplate for this DAO explicitly, + * as an alternative to specifying a SessionFactory. + * @see #setSessionFactory + */ + public final void setHibernateTemplate(HibernateTemplate hibernateTemplate) { + this.hibernateTemplate = hibernateTemplate; + } + + /** + * Return the HibernateTemplate for this DAO, + * pre-initialized with the SessionFactory or set explicitly. + *

Note: The returned HibernateTemplate is a shared instance. + * You may introspect its configuration, but not modify the configuration + * (other than from within an {@link #initDao} implementation). + * Consider creating a custom HibernateTemplate instance via + * {@code new HibernateTemplate(getSessionFactory())}, in which case + * you're allowed to customize the settings on the resulting instance. + */ + public final HibernateTemplate getHibernateTemplate() { + return this.hibernateTemplate; + } + + @Override + protected final void checkDaoConfig() { + if (this.hibernateTemplate == null) { + throw new IllegalArgumentException("'sessionFactory' or 'hibernateTemplate' is required"); + } + } + + + /** + * Conveniently obtain the current Hibernate Session. + * @return the Hibernate Session + * @throws DataAccessResourceFailureException if the Session couldn't be created + * @see SessionFactory#getCurrentSession() + */ + protected final Session currentSession() throws DataAccessResourceFailureException { + return getSessionFactory().getCurrentSession(); + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewFilter.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewFilter.java new file mode 100644 index 0000000000..9324ea4a74 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewFilter.java @@ -0,0 +1,221 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5.support; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.orm.hibernate5.SessionFactoryUtils; +import org.springframework.orm.hibernate5.SessionHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Servlet Filter that binds a Hibernate Session to the thread for the entire + * processing of the request. Intended for the "Open Session in View" pattern, + * i.e. to allow for lazy loading in web views despite the original transactions + * already being completed. + * + *

This filter makes Hibernate Sessions available via the current thread, which + * will be autodetected by transaction managers. It is suitable for service layer + * transactions via {@link org.springframework.orm.hibernate5.HibernateTransactionManager} + * as well as for non-transactional execution (if configured appropriately). + * + *

NOTE: This filter will by default not flush the Hibernate Session, + * with the flush mode set to {@code FlushMode.NEVER}. It assumes to be used + * in combination with service layer transactions that care for the flushing: The + * active transaction manager will temporarily change the flush mode to + * {@code FlushMode.AUTO} during a read-write transaction, with the flush + * mode reset to {@code FlushMode.NEVER} at the end of each transaction. + * + *

WARNING: Applying this filter to existing logic can cause issues that + * have not appeared before, through the use of a single Hibernate Session for the + * processing of an entire request. In particular, the reassociation of persistent + * objects with a Hibernate Session has to occur at the very beginning of request + * processing, to avoid clashes with already loaded instances of the same objects. + * + *

Looks up the SessionFactory in Spring's root web application context. + * Supports a "sessionFactoryBeanName" filter init-param in {@code web.xml}; + * the default bean name is "sessionFactory". + * + * @author Juergen Hoeller + * @since 4.2 + * @see #lookupSessionFactory + * @see OpenSessionInViewInterceptor + * @see OpenSessionInterceptor + * @see org.springframework.orm.hibernate5.HibernateTransactionManager + * @see TransactionSynchronizationManager + * @see SessionFactory#getCurrentSession() + */ +public class OpenSessionInViewFilter extends OncePerRequestFilter { + + public static final String DEFAULT_SESSION_FACTORY_BEAN_NAME = "sessionFactory"; + + private String sessionFactoryBeanName = DEFAULT_SESSION_FACTORY_BEAN_NAME; + + + /** + * Set the bean name of the SessionFactory to fetch from Spring's + * root application context. Default is "sessionFactory". + * @see #DEFAULT_SESSION_FACTORY_BEAN_NAME + */ + public void setSessionFactoryBeanName(String sessionFactoryBeanName) { + this.sessionFactoryBeanName = sessionFactoryBeanName; + } + + /** + * Return the bean name of the SessionFactory to fetch from Spring's + * root application context. + */ + protected String getSessionFactoryBeanName() { + return this.sessionFactoryBeanName; + } + + + /** + * Returns "false" so that the filter may re-bind the opened Hibernate + * {@code Session} to each asynchronously dispatched thread and postpone + * closing it until the very last asynchronous dispatch. + */ + @Override + protected boolean shouldNotFilterAsyncDispatch() { + return false; + } + + /** + * Returns "false" so that the filter may provide a Hibernate + * {@code Session} to each error dispatches. + */ + @Override + protected boolean shouldNotFilterErrorDispatch() { + return false; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + SessionFactory sessionFactory = lookupSessionFactory(request); + boolean participate = false; + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + String key = getAlreadyFilteredAttributeName(); + + if (TransactionSynchronizationManager.hasResource(sessionFactory)) { + // Do not modify the Session: just set the participate flag. + participate = true; + } + else { + boolean isFirstRequest = !isAsyncDispatch(request); + if (isFirstRequest || !applySessionBindingInterceptor(asyncManager, key)) { + logger.debug("Opening Hibernate Session in OpenSessionInViewFilter"); + Session session = openSession(sessionFactory); + SessionHolder sessionHolder = new SessionHolder(session); + TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder); + + AsyncRequestInterceptor interceptor = new AsyncRequestInterceptor(sessionFactory, sessionHolder); + asyncManager.registerCallableInterceptor(key, interceptor); + asyncManager.registerDeferredResultInterceptor(key, interceptor); + } + } + + try { + filterChain.doFilter(request, response); + } + + finally { + if (!participate) { + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.unbindResource(sessionFactory); + if (!isAsyncStarted(request)) { + logger.debug("Closing Hibernate Session in OpenSessionInViewFilter"); + SessionFactoryUtils.closeSession(sessionHolder.getSession()); + } + } + } + } + + /** + * Look up the SessionFactory that this filter should use, + * taking the current HTTP request as argument. + *

The default implementation delegates to the {@link #lookupSessionFactory()} + * variant without arguments. + * @param request the current request + * @return the SessionFactory to use + */ + protected SessionFactory lookupSessionFactory(HttpServletRequest request) { + return lookupSessionFactory(); + } + + /** + * Look up the SessionFactory that this filter should use. + *

The default implementation looks for a bean with the specified name + * in Spring's root application context. + * @return the SessionFactory to use + * @see #getSessionFactoryBeanName + */ + protected SessionFactory lookupSessionFactory() { + if (logger.isDebugEnabled()) { + logger.debug("Using SessionFactory '" + getSessionFactoryBeanName() + "' for OpenSessionInViewFilter"); + } + WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + return wac.getBean(getSessionFactoryBeanName(), SessionFactory.class); + } + + /** + * Open a Session for the SessionFactory that this filter uses. + *

The default implementation delegates to the {@link SessionFactory#openSession} + * method and sets the {@link Session}'s flush mode to "MANUAL". + * @param sessionFactory the SessionFactory that this filter uses + * @return the Session to use + * @throws DataAccessResourceFailureException if the Session could not be created + * @see FlushMode#MANUAL + */ + protected Session openSession(SessionFactory sessionFactory) throws DataAccessResourceFailureException { + try { + Session session = sessionFactory.openSession(); + session.setFlushMode(FlushMode.MANUAL); + return session; + } + catch (HibernateException ex) { + throw new DataAccessResourceFailureException("Could not open Hibernate Session", ex); + } + } + + private boolean applySessionBindingInterceptor(WebAsyncManager asyncManager, String key) { + if (asyncManager.getCallableInterceptor(key) == null) { + return false; + } + ((AsyncRequestInterceptor) asyncManager.getCallableInterceptor(key)).bindSession(); + return true; + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewInterceptor.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewInterceptor.java new file mode 100644 index 0000000000..40b93f9c0e --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewInterceptor.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.orm.hibernate5.SessionFactoryUtils; +import org.springframework.orm.hibernate5.SessionHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.ui.ModelMap; +import org.springframework.web.context.request.AsyncWebRequestInterceptor; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; + +/** + * Spring web request interceptor that binds a Hibernate {@code Session} to the + * thread for the entire processing of the request. + * + *

This class is a concrete expression of the "Open Session in View" pattern, which + * is a pattern that allows for the lazy loading of associations in web views despite + * the original transactions already being completed. + * + *

This interceptor makes Hibernate Sessions available via the current thread, + * which will be autodetected by transaction managers. It is suitable for service layer + * transactions via {@link org.springframework.orm.hibernate5.HibernateTransactionManager} + * as well as for non-transactional execution (if configured appropriately). + * + *

In contrast to {@link OpenSessionInViewFilter}, this interceptor is configured + * in a Spring application context and can thus take advantage of bean wiring. + * + *

WARNING: Applying this interceptor to existing logic can cause issues + * that have not appeared before, through the use of a single Hibernate + * {@code Session} for the processing of an entire request. In particular, the + * reassociation of persistent objects with a Hibernate {@code Session} has to + * occur at the very beginning of request processing, to avoid clashes with already + * loaded instances of the same objects. + * + * @author Juergen Hoeller + * @since 4.2 + * @see OpenSessionInViewFilter + * @see OpenSessionInterceptor + * @see org.springframework.orm.hibernate5.HibernateTransactionManager + * @see TransactionSynchronizationManager + * @see SessionFactory#getCurrentSession() + */ +public class OpenSessionInViewInterceptor implements AsyncWebRequestInterceptor { + + /** + * Suffix that gets appended to the {@code SessionFactory} + * {@code toString()} representation for the "participate in existing + * session handling" request attribute. + * @see #getParticipateAttributeName + */ + public static final String PARTICIPATE_SUFFIX = ".PARTICIPATE"; + + protected final Log logger = LogFactory.getLog(getClass()); + + private SessionFactory sessionFactory; + + + /** + * Set the Hibernate SessionFactory that should be used to create Hibernate Sessions. + */ + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + /** + * Return the Hibernate SessionFactory that should be used to create Hibernate Sessions. + */ + public SessionFactory getSessionFactory() { + return this.sessionFactory; + } + + + /** + * Open a new Hibernate {@code Session} according and bind it to the thread via the + * {@link TransactionSynchronizationManager}. + */ + @Override + public void preHandle(WebRequest request) throws DataAccessException { + String participateAttributeName = getParticipateAttributeName(); + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + if (asyncManager.hasConcurrentResult()) { + if (applySessionBindingInterceptor(asyncManager, participateAttributeName)) { + return; + } + } + + if (TransactionSynchronizationManager.hasResource(getSessionFactory())) { + // Do not modify the Session: just mark the request accordingly. + Integer count = (Integer) request.getAttribute(participateAttributeName, WebRequest.SCOPE_REQUEST); + int newCount = (count != null ? count + 1 : 1); + request.setAttribute(getParticipateAttributeName(), newCount, WebRequest.SCOPE_REQUEST); + } + else { + logger.debug("Opening Hibernate Session in OpenSessionInViewInterceptor"); + Session session = openSession(); + SessionHolder sessionHolder = new SessionHolder(session); + TransactionSynchronizationManager.bindResource(getSessionFactory(), sessionHolder); + + AsyncRequestInterceptor asyncRequestInterceptor = + new AsyncRequestInterceptor(getSessionFactory(), sessionHolder); + asyncManager.registerCallableInterceptor(participateAttributeName, asyncRequestInterceptor); + asyncManager.registerDeferredResultInterceptor(participateAttributeName, asyncRequestInterceptor); + } + } + + @Override + public void postHandle(WebRequest request, ModelMap model) { + } + + /** + * Unbind the Hibernate {@code Session} from the thread and close it). + * @see TransactionSynchronizationManager + */ + @Override + public void afterCompletion(WebRequest request, Exception ex) throws DataAccessException { + if (!decrementParticipateCount(request)) { + SessionHolder sessionHolder = + (SessionHolder) TransactionSynchronizationManager.unbindResource(getSessionFactory()); + logger.debug("Closing Hibernate Session in OpenSessionInViewInterceptor"); + SessionFactoryUtils.closeSession(sessionHolder.getSession()); + + } + } + + 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(getSessionFactory()); + } + } + + /** + * Open a Session for the SessionFactory that this interceptor uses. + *

The default implementation delegates to the {@link SessionFactory#openSession} + * method and sets the {@link Session}'s flush mode to "MANUAL". + * @return the Session to use + * @throws DataAccessResourceFailureException if the Session could not be created + * @see FlushMode#MANUAL + */ + protected Session openSession() throws DataAccessResourceFailureException { + try { + Session session = getSessionFactory().openSession(); + session.setFlushMode(FlushMode.MANUAL); + return session; + } + catch (HibernateException ex) { + throw new DataAccessResourceFailureException("Could not open Hibernate Session", ex); + } + } + + /** + * Return the name of the request attribute that identifies that a request is + * already intercepted. + *

The default implementation takes the {@code toString()} representation + * of the {@code SessionFactory} instance and appends {@link #PARTICIPATE_SUFFIX}. + */ + protected String getParticipateAttributeName() { + return getSessionFactory().toString() + PARTICIPATE_SUFFIX; + } + + private boolean applySessionBindingInterceptor(WebAsyncManager asyncManager, String key) { + if (asyncManager.getCallableInterceptor(key) == null) { + return false; + } + ((AsyncRequestInterceptor) asyncManager.getCallableInterceptor(key)).bindSession(); + return true; + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInterceptor.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInterceptor.java new file mode 100644 index 0000000000..dd1c7c16b5 --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInterceptor.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.hibernate5.support; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.orm.hibernate5.SessionFactoryUtils; +import org.springframework.orm.hibernate5.SessionHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Simple AOP Alliance {@link MethodInterceptor} implementation that binds a new + * Hibernate {@link Session} for each method invocation, if none bound before. + * + *

This is a simple Hibernate Session scoping interceptor along the lines of + * {@link OpenSessionInViewInterceptor}, just for use with AOP setup instead of + * MVC setup. It opens a new {@link Session} with flush mode "MANUAL" since the + * Session is only meant for reading, except when participating in a transaction. + * + * @author Juergen Hoeller + * @since 4.2 + * @see OpenSessionInViewInterceptor + * @see OpenSessionInViewFilter + * @see org.springframework.orm.hibernate5.HibernateTransactionManager + * @see TransactionSynchronizationManager + * @see SessionFactory#getCurrentSession() + */ +public class OpenSessionInterceptor implements MethodInterceptor, InitializingBean { + + private SessionFactory sessionFactory; + + + /** + * Set the Hibernate SessionFactory that should be used to create Hibernate Sessions. + */ + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + /** + * Return the Hibernate SessionFactory that should be used to create Hibernate Sessions. + */ + public SessionFactory getSessionFactory() { + return this.sessionFactory; + } + + @Override + public void afterPropertiesSet() { + if (getSessionFactory() == null) { + throw new IllegalArgumentException("Property 'sessionFactory' is required"); + } + } + + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + SessionFactory sf = getSessionFactory(); + if (!TransactionSynchronizationManager.hasResource(sf)) { + // New Session to be bound for the current method's scope... + Session session = openSession(); + try { + TransactionSynchronizationManager.bindResource(sf, new SessionHolder(session)); + return invocation.proceed(); + } + finally { + SessionFactoryUtils.closeSession(session); + TransactionSynchronizationManager.unbindResource(sf); + } + } + else { + // Pre-bound Session found -> simply proceed. + return invocation.proceed(); + } + } + + /** + * Open a Session for the SessionFactory that this interceptor uses. + *

The default implementation delegates to the {@link SessionFactory#openSession} + * method and sets the {@link Session}'s flush mode to "MANUAL". + * @return the Session to use + * @throws DataAccessResourceFailureException if the Session could not be created + * @see FlushMode#MANUAL + */ + protected Session openSession() throws DataAccessResourceFailureException { + try { + Session session = getSessionFactory().openSession(); + session.setFlushMode(FlushMode.MANUAL); + return session; + } + catch (HibernateException ex) { + throw new DataAccessResourceFailureException("Could not open Hibernate Session", ex); + } + } + +} diff --git a/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/package-info.java b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/package-info.java new file mode 100644 index 0000000000..7c7c5a9efc --- /dev/null +++ b/spring-orm-hibernate5/src/main/java/org/springframework/orm/hibernate5/support/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes supporting the {@code org.springframework.orm.hibernate5} package. + */ +package org.springframework.orm.hibernate5.support; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java index d5c498e843..44aff3d11c 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,8 +34,8 @@ import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.SQLServerDialect; /** - * {@link org.springframework.orm.jpa.JpaVendorAdapter} implementation for - * Hibernate EntityManager. Developed and tested against Hibernate 3.6 and 4.2/4.3. + * {@link org.springframework.orm.jpa.JpaVendorAdapter} implementation for Hibernate + * EntityManager. Developed and tested against Hibernate 3.6, 4.2/4.3 as well as 5.0. * Hibernate 4.2+ is strongly recommended for use with Spring 4.0+. * *

Exposes Hibernate's persistence provider and EntityManager extension interface, @@ -79,7 +79,7 @@ public class HibernateJpaVendorAdapter extends AbstractJpaVendorAdapter { PersistenceProvider providerToUse; try { try { - // Try Hibernate 4.3's org.hibernate.jpa package in order to avoid deprecation warnings + // Try Hibernate 4.3/5.0's org.hibernate.jpa package in order to avoid deprecation warnings emfIfcToUse = (Class) cl.loadClass("org.hibernate.jpa.HibernateEntityManagerFactory"); emIfcToUse = (Class) cl.loadClass("org.hibernate.jpa.HibernateEntityManager"); providerClass = cl.loadClass("org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider");