java - manager - spring transaction rollback




Spring @Transactional(Propagation.NEVER) dovrebbe creare una sessione di sospensione? (3)

Supponiamo di aver correttamente configurato jpa backed by hibernate (4.3.11) in primavera (4.2.7). La cache di primo livello di ibernazione è abilitata. Usiamo transazioni dichiarative. Abbiamo 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 che viene chiamato da OuterBean:

@Service
public class InnerBean {

    @Resource
    private UserDao userDao;

    @Transactional
    public void withTransaction(){
        User user = userDao.load(1l);
        user.setName("newUserName");
    }

}

È corretto che il metodo user.getName() in OuterBean restituisca lo stesso valore due volte (la seconda volta è dopo il nome dell'aggiornamento nel database)?

In altre parole, è il comportamento corretto che @Transactional(propagation = Propagation.NEVER) crea la sessione di ibernazione per il metodo withoutTransaction() che causa la user.getName() seconda chiamata user.getName() dalla cache di primo livello in ibernazione anziché dal database?

MODIFICARE

Per spiegare meglio il problema, attache traccia dalla creazione di sessioni di ibernazione

TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl  - Opening Hibernate Session.  tenant=null, owner=org.hibernate.jpa.internal.[email protected]
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.[email protected]
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

Ora confrontiamo la traccia quando rimuovo @Transactional(propagation = Propagation.NEVER)

TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl  - Opening Hibernate Session.  tenant=null, owner=org.hibernate.jpa.internal.[email protected]
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.[email protected]
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.[email protected]
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

Notare che quando @Transactional(propagation = Propagation.NEVER) sessione separata è creta per ogni invocazione del metodo da parte dell'utente.

Quindi la mia domanda può essere formulata anche come non dovrebbe essere @Transactional(propagation = Propagation.NEVER) implementato in primavera come guardiano che ci impedisce di usare accidentalmente la transazione, senza alcun effetto collaterale (creazione di sessioni)?


@Transactional (propagation = Propagation.NEVER) creerebbe comunque una sessione. Se si utilizza la combinazione Spring / Hibernate / JPA per le transazioni non distribuite, si utilizza sicuramente JpaTransactionManager come gestore delle transazioni Spring. La risposta alla tua domanda si trova in questa classe. Una buona idea sarebbe usare un debugger nel tuo IDE per seguire cosa sta succedendo. Il metodo doBegin di questa classe (che è chiamato da infrastruttura di transazione Spring è: -

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);
        }
    }

La risorsa transazionale quando si utilizza JPA è in realtà il gestore di entità (l'implementazione sottostante è la sessione in modalità ibernazione), come si può vedere e questa è la prima cosa che questo metodo fa

EntityManager em = txObject.getEntityManagerHolder().getEntityManager();

Quindi, sicuramente viene creato un gestore / sessione di entità. Gli attributi della transazione vengono quindi passati al sottostante JpaDialect (HibernateJpaDialect) tramite TransactionDefinition. A sua volta, questa classe ottiene in realtà la sessione di ibernazione sottostante e l'API di transazione della sessione.

HibernateJpaDialect {
........
public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition)
Session session = getSession(entityManager);
entityManager.getTransaction().begin();
......
......
}
......

Il comportamento è corretto: Hibernate creerà sempre una sessione (in quale altro modo ti aspetteresti che esegua qualsiasi operazione?) E caricando l'entità che l'hai associata a quella sessione. Poiché senza withoutTransaction non partecipa a una transazione, le modifiche apportate all'interno di withTransaction avverranno all'interno di una nuova transazione e non dovrebbero essere visibili a meno che non si chiami l' refresh , il che costringerà a ricaricarsi dal database.

Sto citando la documentazione ufficiale di Hibernate :

La funzione principale della Sessione è quella di offrire operazioni di creazione, lettura ed eliminazione per istanze di classi entità mappate. Le istanze possono esistere in uno dei tre stati:

  • transitorio: mai persistente, non associato a nessuna sessione
  • persistente: associato a una sessione unica staccato: in precedenza
  • persistente, non associato a nessuna sessione

Le istanze temporanee possono essere rese persistenti chiamando save() , persist() o saveOrUpdate() . Le istanze persistenti possono essere rese transitori chiamando delete() . Qualsiasi istanza restituita da un metodo get() o load() è persistente.

Tratto da Java Persistence With Hibernate, Second Edition di Christian Bauer, Gavin King e Gary Gregory:

Il contesto di persistenza agisce come una cache di primo livello; ricorda tutte le istanze di entità che hai gestito in una particolare unità di lavoro . Ad esempio, se si chiede a Hibernate di caricare un'istanza di entità utilizzando un valore di chiave primaria (una ricerca per identificatore), Hibernate può innanzitutto controllare l'unità di lavoro corrente nel contesto di persistenza. Se Hibernate trova l'istanza dell'entità nel contesto di persistenza, non si verifica alcun hit del database, si tratta di una lettura ripetibile per un'applicazione . Le em.find(Item.class, ITEM_ID) consecutive em.find(Item.class, ITEM_ID) con lo stesso contesto di persistenza producono lo stesso risultato.

Anche da Java Persistence With Hibernate, Second Edition :

La cache del contesto di persistenza è sempre attiva e non può essere disattivata. Garantisce quanto segue:

  • Il livello di persistenza non è vulnerabile allo nel caso di riferimenti circolari in un grafo di oggetti.
  • Non ci possono mai essere rappresentazioni in conflitto della stessa riga del database alla fine di un'unità di lavoro. Il provider può scrivere in modo sicuro tutte le modifiche apportate a un'istanza di entità nel database.
  • Allo stesso modo, le modifiche apportate in un particolare contesto di persistenza sono sempre immediatamente visibili a tutti gli altri codici eseguiti all'interno di quell'unità di lavoro e al relativo contesto di persistenza. JPA garantisce letture di istanze di entità ripetibili.

Per quanto riguarda le transazioni, ecco un estratto dalla documentazione ufficiale di Hibernate :

Definisce il contratto per l'astrazione delle applicazioni dai mezzi sottostanti configurati di gestione delle transazioni. Consente all'applicazione di definire le unità di lavoro , pur mantenendo l'astrazione dall'implementazione della transazione sottostante (ad esempio JTA, JDBC).

Quindi, per riassumere, withTransaction e withoutTransaction non condivideranno UnitOfWork e quindi non condivideranno la cache di primo livello, motivo per cui il secondo carico restituisce il valore originale.

Per quanto riguarda i motivi per cui questi due metodi non condividono l'unità di lavoro, puoi fare riferimento alla risposta di Shailendra.

MODIFICARE:

Sembri fraintendere qualcosa. Una sessione deve sempre essere creata - ecco come funziona Hibernate, punto. La tua aspettativa di nessuna sessione in fase di creazione equivale a prevedere di eseguire una query JDBC senza avere una connessione JDBC :)

La differenza tra i due esempi è che con @Transactional(propagation = Propagation.NEVER) il metodo viene intercettato e inoltrato da Spring e viene creata una sola sessione per le query in withoutTransaction . Quando si rimuove l'annotazione, si esclude il metodo dall'intercettore transazionale di Spring in modo da creare una nuova sessione per ogni operazione relativa al DB. Ripeto di nuovo, e non posso sottolineare questo abbastanza - è necessario avere una sessione aperta per eseguire qualsiasi domanda.

Per quanto riguarda la protezione, prova a scambiare le annotazioni sui due metodi facendo in withTransaction che con withTransaction usi Propagation.NEVER e withoutTransaction usi l'annotazione @Transactional predefinita e vedi cosa succede (spoiler: otterrai una IllegalTransactionStateException ).

EDIT2:

Per quanto riguarda il motivo per cui la sessione è condivisa tra due carichi nel bean esterno - è proprio quello che si suppone faccia JpaTransactionManager , e annotando il metodo con @Transactional hai avvisato Spring che dovrebbe usare il gestore delle transazioni configurato per avvolgere il tuo metodo. Ecco cosa dice la documentazione ufficiale sul comportamento previsto di JpaTransactionManager :

Implementazione PlatformTransactionManager per un singolo EntityManagerFactory JPA. Associa un EntityManager JPA dal factory specificato al thread , consentendo potenzialmente un EntityManager associato al thread per factory . SharedEntityManagerCreator e @PersistenceContext sono a conoscenza dei gestori di entità legate a thread e partecipano automaticamente a tali transazioni. L'utilizzo di entrambi è necessario per il codice di accesso JPA che supporta questo meccanismo di gestione delle transazioni.

Inoltre, per sapere come Spring gestisce la gestione dichiarativa delle transazioni (ad esempio annotazioni di @Transactional sui metodi), fare riferimento alla documentazione ufficiale . Per facilitare la navigazione, includerò una citazione:

I concetti più importanti da comprendere in merito al supporto dichiarativo delle transazioni di Spring Framework sono che questo supporto è abilitato tramite proxy AOP e che il consiglio transazionale è guidato dai metadati (attualmente basati su XML o annotazioni). La combinazione di AOP con metadati transazionali genera un proxy AOP che utilizza un TransactionInterceptor in combinazione con un'implementazione PlatformTransactionManager appropriata per indirizzare le transazioni attorno alle invocazioni dei metodi .


Prima di tutto, quando si utilizza l'ibernazione dietro l'API JPA, userò il termine EntityManager invece della sessione (rigorosamente la stessa cosa, solo una questione di terminologia).

Ogni accesso al database tramite JPA coinvolgerà un EntityManager , si stanno recuperando entità, è necessario un EntityManager (EM). La cosiddetta cache di 1 ° livello non è altro che lo stato delle entità gestite EM.

Teoricamente, il ciclo di vita dell'EM è breve e legato a un'unità di lavoro (e quindi in generale a una transazione, vedere Lottare per capire l'uso corretto di EntityManager ).

Ora è possibile utilizzare JPA in modo diverso: persistenza gestita dal contenitore o gestita dall'utente. Quando EM è gestito dal container (il tuo caso, qui spring è il contenitore) quest'ultimo è responsabile della gestione dell'ambito EM / ciclo di vita (crea, svuota e distruggi per te). Poiché EM è limitato a una transazione / Unità di lavoro, questa attività è delegata al TransactionManager (l'oggetto che gestisce le annotazioni @Transactional ).

Quando si annota un metodo utilizzando @Transactional(propagation = Propagation.NEVER) , si sta creando un ambito di transazione logico di primavera che assicurerà che non vi sia alcuna transazione JDBC sottostante associata a un EM esistente eventualmente, che non ne creerà uno e utilizzerà Modalità di autocommit JDBC ma che creerà un EM per questo ambito di transazione logico se non ne esiste già uno.

Per quanto riguarda il fatto che viene creata una nuova istanza EM per ciascuna chiamata DAO quando non è definito alcun ambito logico della transazione, è necessario ricordare che non è possibile accedere al database utilizzando JPA al di fuori dell'EM. In questo caso, la sospensione di AFAIK veniva utilizzata per eseguire una no session bound to thread errore di no session bound to thread ma questo potrebbe essersi evoluto con versioni successive, altrimenti il ​​DAO potrebbe essere annotato con @Transactional(propagation = Propagation.SUPPORT) che creerebbe automaticamente un EM se non si racchiude esiste un ambito logico. Questa è una cattiva pratica in quanto la transazione dovrebbe essere definita nell'unità di lavoro, ad es. il livello di servizio e non quello DAO.





jpa