java - example - Spring @Transactional(Propagation.NEVER) debe crear una sesión de hibernación?
spring boot transaction rollback (4)
Supongamos que hemos configurado correctamente jpa respaldado por hibernación (4.3.11) en primavera (4.2.7). Hibernate caché de primer nivel está habilitado. Utilizamos transacciones declarativas. Tenemos OuterBean
@Service
public class OuterBean {
@Resource
private UserDao userDao;
@Resource
private InnerBean innerBean;
@Transactional(propagation = Propagation.NEVER)
public void withoutTransaction(){
User user = userDao.load(1l);
System.out.println(user.getName());//return userName
innerBean.withTransaction();
user = userDao.load(1l);
System.out.println(user.getName());//return userName instead of newUserName
}
}
E InnerBean que se llama desde OuterBean:
@Service
public class InnerBean {
@Resource
private UserDao userDao;
@Transactional
public void withTransaction(){
User user = userDao.load(1l);
user.setName("newUserName");
}
}
¿Es correcto el comportamiento que el método user.getName()
en OuterBean devuelve el mismo valor dos veces (la segunda vez es después del nombre de actualización en la base de datos)?
En otras palabras, ¿es correcto el comportamiento que @Transactional(propagation = Propagation.NEVER)
crea una sesión de hibernación para el método withoutTransaction()
que hace que la segunda llamada user.getName()
lea desde el caché de hibernación de primer nivel en lugar de la base de datos?
EDITAR
Para explicar el problema más adjunto el seguimiento de la creación de sesiones de hibernación
TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl - Opening Hibernate Session. tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@c17285e
TRACE org.hibernate.internal.SessionImpl - Opened session at timestamp: 14689173439
TRACE org.hibernate.internal.SessionImpl - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
userName
TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl - Opening Hibernate Session. tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@715c48ca
TRACE org.hibernate.internal.SessionImpl - Opened session at timestamp: 14689173439
TRACE org.hibernate.internal.SessionImpl - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Automatically flushing session
TRACE org.hibernate.internal.SessionImpl - before transaction completion
TRACE org.hibernate.internal.SessionImpl - after transaction completion
TRACE org.hibernate.internal.SessionImpl - Closing session
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
userName
TRACE org.hibernate.internal.SessionImpl - Closing session
Ahora comparemos la traza cuando elimine @Transactional(propagation = Propagation.NEVER)
TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl - Opening Hibernate Session. tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@4ebd2c5f
TRACE org.hibernate.internal.SessionImpl - Opened session at timestamp: 14689203905
TRACE org.hibernate.internal.SessionImpl - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Closing session
userName
TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl - Opening Hibernate Session. tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@5af84083
TRACE org.hibernate.internal.SessionImpl - Opened session at timestamp: 14689203905
TRACE org.hibernate.internal.SessionImpl - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Automatically flushing session
TRACE org.hibernate.internal.SessionImpl - before transaction completion
TRACE org.hibernate.internal.SessionImpl - after transaction completion
TRACE org.hibernate.internal.SessionImpl - Closing session
TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl - Opening Hibernate Session. tenant=null, owner=org.hibernate.jpa.internal.EntityManagerImpl@35f4f41f
TRACE org.hibernate.internal.SessionImpl - Opened session at timestamp: 14689203906
TRACE org.hibernate.internal.SessionImpl - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl - Closing session
newUserName
Tenga en cuenta que cuando @Transactional(propagation = Propagation.NEVER)
sesión por separado es muy clara para cada invocación de método desde userDao.
¿Entonces mi pregunta puede formularse también como No debe @Transactional(propagation = Propagation.NEVER)
en Spring como tutor que nos impide utilizar transacciones de forma accidental, sin ningún efecto secundario (creación de sesión)?
@Transactional (propagation = Propagation.NEVER) aún crearía una sesión. Si está utilizando la combinación Spring / Hibernate / JPA para transacciones no distribuidas, lo más seguro es que esté utilizando JpaTransactionManager como administrador de transacciones de Spring. La respuesta a su pregunta se encuentra en esta clase. Una buena idea sería utilizar un depurador en su IDE para seguir lo que está sucediendo. El método doBegin de esta clase (que llama la infraestructura de transacciones de Spring es: -
protected void doBegin(Object transaction, TransactionDefinition definition) {
JpaTransactionObject txObject = (JpaTransactionObject) transaction;
if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
throw new IllegalTransactionStateException(
"Pre-bound JDBC Connection found! JpaTransactionManager does not support " +
"running within DataSourceTransactionManager if told to manage the DataSource itself. " +
"It is recommended to use a single JpaTransactionManager for all transactions " +
"on a single DataSource, no matter whether JPA or JDBC access.");
}
try {
if (txObject.getEntityManagerHolder() == null ||
txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) {
EntityManager newEm = createEntityManagerForTransaction();
if (logger.isDebugEnabled()) {
logger.debug("Opened new EntityManager [" + newEm + "] for JPA transaction");
}
txObject.setEntityManagerHolder(new EntityManagerHolder(newEm), true);
}
EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
// Delegate to JpaDialect for actual transaction begin.
final int timeoutToUse = determineTimeout(definition);
Object transactionData = getJpaDialect().beginTransaction(em,
new DelegatingTransactionDefinition(definition) {
@Override
public int getTimeout() {
return timeoutToUse;
}
});
txObject.setTransactionData(transactionData);
// Register transaction timeout.
if (timeoutToUse != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getEntityManagerHolder().setTimeoutInSeconds(timeoutToUse);
}
// Register the JPA EntityManager''s JDBC Connection for the DataSource, if set.
if (getDataSource() != null) {
ConnectionHandle conHandle = getJpaDialect().getJdbcConnection(em, definition.isReadOnly());
if (conHandle != null) {
ConnectionHolder conHolder = new ConnectionHolder(conHandle);
if (timeoutToUse != TransactionDefinition.TIMEOUT_DEFAULT) {
conHolder.setTimeoutInSeconds(timeoutToUse);
}
if (logger.isDebugEnabled()) {
logger.debug("Exposing JPA transaction as JDBC transaction [" +
conHolder.getConnectionHandle() + "]");
}
TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
txObject.setConnectionHolder(conHolder);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Not exposing JPA transaction [" + em + "] as JDBC transaction because " +
"JpaDialect [" + getJpaDialect() + "] does not support JDBC Connection retrieval");
}
}
}
// Bind the entity manager holder to the thread.
if (txObject.isNewEntityManagerHolder()) {
TransactionSynchronizationManager.bindResource(
getEntityManagerFactory(), txObject.getEntityManagerHolder());
}
txObject.getEntityManagerHolder().setSynchronizedWithTransaction(true);
}
catch (TransactionException ex) {
closeEntityManagerAfterFailedBegin(txObject);
throw ex;
}
catch (Throwable ex) {
closeEntityManagerAfterFailedBegin(txObject);
throw new CannotCreateTransactionException("Could not open JPA EntityManager for transaction", ex);
}
}
El recurso transaccional cuando se usa JPA es en realidad el administrador de entidades (la implementación subyacente es sesión en hibernación), como puede ver, y esto es lo primero que hace este método.
EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
Así que definitivamente se crea un gestor de entidad / sesión. Los atributos de la transacción luego se pasan al JpaDialect subyacente (HibernateJpaDialect) a través de TransactionDefinition. Esta clase, a su vez, obtiene la sesión de Hibernate subyacente y la API de transacción de la sesión.
HibernateJpaDialect {
........
public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition)
Session session = getSession(entityManager);
entityManager.getTransaction().begin();
......
......
}
......
El comportamiento es correcto: Hibernate siempre creará una sesión (¿de qué otra forma esperaría que realizara alguna operación?), Y al cargar la entidad que la ha asociado con esa sesión. Dado que withoutTransaction
no participa en una transacción, los cambios realizados dentro de withTransaction
ocurrirán dentro de una nueva transacción y no deberían ser visibles a menos que llame a la refresh
, lo que forzará una refresh
de la base de datos.
Estoy citando la documentación oficial de Hibernate :
La función principal de la sesión es ofrecer operaciones de creación, lectura y eliminación para instancias de clases de entidades asignadas. Las instancias pueden existir en uno de tres estados:
- Transitoria: nunca persistente, no asociada a ninguna sesión.
- persistente: asociado con una sesión única desconectada: anteriormente
- Persistente, no asociado a ninguna sesión.
Las instancias transitorias pueden hacerse persistentes llamando a
save()
,saveOrUpdate()
osaveOrUpdate()
. Las instancias persistentes pueden hacerse transitorias llamando adelete()
. Cualquier instancia devuelta por un métodoget()
oload()
es persistente.
Tomado de Java Persistence With Hibernate, segunda edición de Christian Bauer, Gavin King y Gary Gregory:
El contexto de persistencia actúa como un caché de primer nivel; recuerda todas las instancias de entidad que ha manejado en una unidad de trabajo particular . Por ejemplo, si le pide a Hibernate que cargue una instancia de entidad usando un valor de clave principal (una búsqueda por identificador), Hibernate puede verificar primero la unidad de trabajo actual en el contexto de persistencia. Si Hibernate encuentra la instancia de la entidad en el contexto de persistencia, no se produce un golpe de base de datos, esta es una lectura repetible para una aplicación . Las
em.find(Item.class, ITEM_ID)
consecutivas deem.find(Item.class, ITEM_ID)
con el mismo contexto de persistencia darán el mismo resultado.
También de Java Persistence With Hibernate, Segunda Edición :
La memoria caché de contexto de persistencia está siempre activada, no se puede desactivar. Se asegura lo siguiente:
- La capa de persistencia no es vulnerable a los desbordamientos de pila en el caso de referencias circulares en un gráfico de objetos.
- Nunca puede haber representaciones en conflicto de la misma fila de la base de datos al final de una unidad de trabajo. El proveedor puede escribir de forma segura todos los cambios realizados en una instancia de entidad en la base de datos.
- Del mismo modo, los cambios realizados en un contexto de persistencia particular siempre son visibles inmediatamente para todos los demás códigos ejecutados dentro de esa unidad de trabajo y su contexto de persistencia. JPA garantiza lecturas de instancia de entidad repetibles.
Con respecto a las transacciones, aquí hay un extracto tomado de la documentación oficial de Hibernate :
Define el contrato para abstraer aplicaciones de los medios subyacentes configurados para la administración de transacciones. Permite que la aplicación defina unidades de trabajo , al mismo tiempo que mantiene la abstracción de la implementación de la transacción subyacente (por ejemplo, JTA, JDBC).
Entonces, para resumir, withTransaction
y withoutTransaction
no compartirán UnitOfWork y, por lo tanto, no compartirán el caché de primer nivel, por lo que la segunda carga devuelve el valor original.
En cuanto a las razones por las que estos dos métodos no comparten la unidad de trabajo, puede consultar la respuesta de Shailendra.
EDITAR:
Pareces malinterpretar algo. Siempre se debe crear una sesión; así es como funciona Hibernate, punto. Su expectativa de que no se creen sesiones es igual a esperar ejecutar una consulta JDBC sin tener una conexión JDBC :)
La diferencia entre sus dos ejemplos es que con @Transactional(propagation = Propagation.NEVER)
su método es interceptado y procesado por Spring y solo se crea una sesión para las consultas en withoutTransaction
. Cuando elimina la anotación, excluye su método del interceptor transaccional de Spring, por lo que se creará una nueva sesión para cada operación relacionada con la base de datos. Repito nuevamente, y no puedo enfatizar esto lo suficiente: debe tener una sesión abierta para realizar cualquier consulta.
En lo que respecta a la protección, intente intercambiar las anotaciones en los dos métodos haciendo que con withTransaction
use Propagation. withoutTransaction
y sin withoutTransaction
use la anotación predeterminada @Transactional
y vea qué sucede (spoiler: obtendrá una IllegalTransactionStateException
).
EDIT2:
En cuanto a por qué la sesión se comparte entre dos cargas en el bean externo: eso es exactamente lo que JpaTransactionManager
debe hacer, y al anotar su método con @Transactional
notificó a Spring que debería usar el administrador de transacciones configurado para ajustar su método. JpaTransactionManager
es lo que dice la documentación oficial sobre el comportamiento esperado de JpaTransactionManager
:
Implementación de PlatformTransactionManager para una sola JPA EntityManagerFactory. Enlaza un EntityManager JPA de la fábrica especificada al subproceso , permitiendo potencialmente un EntityManager enlazado a un subproceso por fábrica . SharedEntityManagerCreator y @PersistenceContext son conscientes de los administradores de entidades vinculados a hilos y participan en dichas transacciones de forma automática. El uso de cualquiera de los dos es obligatorio para el código de acceso JPA que admite este mecanismo de administración de transacciones.
Además, para saber cómo maneja Spring la gestión declarativa de las transacciones (es decir, @Transactional
anotaciones @Transactional
sobre los métodos), consulte la documentación oficial . Para facilitar la navegación, incluiré una cita:
Los conceptos más importantes que se deben comprender con respecto al soporte declarativo de transacciones de Spring Framework son que este soporte se habilita a través de proxies AOP, y que el asesoramiento transaccional está impulsado por metadatos (actualmente basados en anotaciones o XML). La combinación de AOP con metadatos transaccionales produce un proxy AOP que utiliza un
TransactionInterceptor
junto con una implementación dePlatformTransactionManager
adecuada para dirigir las transacciones en torno a las invocaciones de métodos .
En primer lugar, a medida que usa hibernación detrás de la API de JPA, EntityManager
el término EntityManager
lugar de sesión (estrictamente lo mismo, solo una cuestión de terminología).
Cada acceso a la base de datos utilizando JPA implicará un EntityManager
, usted está buscando entidades, necesita un EntityManager
(EM). Lo que se denomina caché de primer nivel no es más que el estado de las entidades gestionadas de EM.
Teóricamente, el ciclo de vida del EM es corto y está vinculado a una unidad de trabajo (y, por lo general, a una transacción, vea Lucha para entender el uso adecuado del EntityManager ).
Ahora JPA se puede utilizar de diferentes maneras: persistencia gestionada por el contenedor o gestionada por el usuario. Cuando el EM es administrado por el contenedor (su caso, aquí es el contenedor), este último se encarga de administrar el alcance / ciclo de vida del EM (crear, vaciar y destruirlo por usted). Como el EM está limitado a una transacción / Unidad de trabajo, esta tarea se delega al TransactionManager
(el objeto que maneja las anotaciones @Transactional
).
Cuando anota un método utilizando @Transactional(propagation = Propagation.NEVER)
, está creando un ámbito de transacción lógica de resorte que asegurará que no haya una transacción JDBC subyacente existente vinculada a un EM existente eventual, que no creará uno y usará Modo de confirmación automática de JDBC, pero que creará un EM para este ámbito de transacción lógica si no existe ninguno.
Con respecto al hecho de que se crea una nueva instancia de EM para cada llamada DAO cuando no se define un ámbito lógico de transacción, debe recordar que no puede acceder a la base de datos utilizando JPA fuera del EM. En este caso, la hibernación de AFAIK se utiliza para lanzar un error de no session bound to thread
pero esto puede haber evolucionado con versiones posteriores. De lo contrario, su DAO puede anotarse con @Transactional(propagation = Propagation.SUPPORT)
que también crearía automáticamente un EM si no se incluye. El alcance lógico existe. Esta es una mala práctica ya que la transacción debe definirse en la unidad de trabajo, por ejemplo. El nivel de servicio y no el de DAO.
No creo que este sea el comportamiento correcto. Es cierto lo que dicen los colegas de que incluso sin transacción, la hibernación está creando una sesión. Pero esto significa que estamos enfrentando dos sesiones S1 y S2 para las dos lecturas separadas de la DAO. Al mismo tiempo, el caché L1 siempre es por sesión, por lo que no tiene sentido que dos sesiones separadas tengan un resultado para el caché L1. Parece que su Spring no está respetando el @Transactional (propagation = Propagation.NEVER)
El @Transactional (propagation = Propagation.NEVER) debe ser equivalente a si simplemente inicia su servicio desde un método principal y realiza las llamadas subsiguientes al DAO usted mismo.
Pruébelo en una clase principal y vea cómo reaccionará. Dudo que vuelva a golpear el caché L1.
También copiaré y pegaré el documento de Sprint en la propagación NUNCA:
NUNCA ejecute de forma no transaccional, lance una excepción si existe una transacción.
Una pregunta más: ¿está la hibernación configurada para AutoCommit? ¿Es posible que el método "runInTransaction" no se esté confirmando?