2016-07-13 26 views
21

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 OuterBeanSpring @Transactional (Propagation.NEVER) dovrebbe creare una sessione di sospensione?

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

}

E 'un comportamento corretto che il metodo user.getName() nel OuterBean restituisce lo stesso valore due volte (la seconda volta è dopo aggiornare il nome nel database)?

In altre parole è vero comportamento corretto che @Transactional(propagation = Propagation.NEVER) crea hibernate sessione per il metodo withoutTransaction() che provoca che in seconda convocazione user.getName() legge dalla cache di primo livello di ibernazione invece di database?

EDIT

Per spiegare problema più mi Attaché traccia dalla creazione di sessioni Hibernate

TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl - Opening Hibernate Session. tenant=null, [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, [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 traccia quando rimuovo @Transactional(propagation = Propagation.NEVER)

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

Si prega di notare quando Tralascio @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 custode che ci impedisce di utilizzare accidentalmente la transazione, senza alcun effetto collaterale (creazione di sessione)?

+0

in withTransaction dovresti chiamare userDao.merge (utente) per vedere le modifiche in user.getName() – mommcilo

+0

Non capisco come chiamare 'userDao.merge (utente)' potrebbe aiutare. Dopo l'invocazione 'withTransaction()' newUserName è correttamente inserito nel database. Il problema è che all'interno di 'withoutTransaction()' seconda chiamata di 'user.getName()' ** usa la cache di primo livello in ibernazione ** (a causa di '@Transactional (propagation = Propagation.NEVER)' crea una sessione di ibernazione) invece di ottenere newUserName database di moduli diretti. – snieguu

+1

hibernate utilizza sempre la cache di primo livello, a meno che non venga cancellata. –

risposta

13

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é withoutTransaction non partecipa a una transazione, le modifiche apportate entro withTransaction si verificheranno in una nuova transazione e non dovrebbero essere visibili a meno che non si chiami refresh, che imporrà un nuovo caricamento dal database.

cito Hibernate's official documentation:

La funzione principale della sessione è quello di offrire creare, leggere e cancellare le operazioni per le istanze di classi di entità mappate. Istanze possono esistere in uno dei tre stati:

  • transitori: mai persistente, non associato ad altri Session
  • persistente: associato con una sessione unica distaccato: precedentemente
  • persistente, non associato ad altri Session

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

Tratto da Java Persistence With Hibernate, Second Edition da 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 nel database: si tratta di una lettura ripetibile per un'applicazione. Le chiamate consecutive em.find(Item.class, ITEM_ID) con lo stesso contesto di persistenza produrranno lo stesso risultato.

Anche da Java Persistence With Hibernate, Second Edition:

La cache contesto di persistenza è sempre on-non può essere disattivata. Garantisce quanto segue:

  • Il livello di persistenza non è vulnerabile allo stack overflow nel caso di riferimenti circolari in un grafico oggetto.
  • Non ci possono mai essere rappresentazioni in conflitto della stessa riga di 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 tratto da official Hibernate's documentation:

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

Quindi, per riassumere, withTransaction e withoutTransaction non condividerà UnitOfWork e quindi non condividere la cache di primo livello, che è il 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, è possibile fare riferimento alla risposta di Shailendra.

EDIT:

Sembra che tu fraintendere qualcosa. Una sessione deve sempre essere creata - ecco come funziona Hibernate, punto.L'aspettativa che nessuna sessione venga creata equivale a prevedere una query JDBC senza una connessione JDBC :)

La differenza tra i due esempi è che con @Transactional(propagation = Propagation.NEVER) il metodo viene intercettato e inoltrato da Spring e solo una singola sessione viene creato per le query in withoutTransaction. Quando si rimuove l'annotazione, si esclude il metodo dall'intercettatore 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 query.

Per quanto riguarda la guardia va - provare a scambiare le annotazioni sui due metodi facendo withTransaction uso Propagation.NEVER e withoutTransaction usare l'annotazione di default @Transactional e vedere cosa succede (spoiler: si otterrà un IllegalTransactionStateException).

EDIT2:

Per quanto riguarda il motivo per cui la sessione è condivisa tra due carichi nel chicco esterno - è proprio quello che JpaTransactionManager dovrebbe fare, e annotando il metodo con @Transactional hai notificato primavera che dovrebbe utilizzare il gestore transazioni configurato per includere il metodo. Ecco cosa dice a proposito the official documentationJpaTransactionManager 's comportamento previsto:

implementazione PlatformTransactionManager per un singolo JPA EntityManagerFactory. Associa un EntityManager JPA dal factory specificato alla thread, consentendo potenzialmente EntityManager thread-bound 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 sta gestendo primavera gestione dichiarativa delle transazioni (ovvero @Transactional annotazioni sui metodi), si riferiscono alla official documentation. Per la facilità di navigazione, io includo una citazione:

I concetti più importanti da cogliere per quanto riguarda il supporto delle transazioni dichiarativa del 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 uno TransactionInterceptor in combinazione con un'implementazione PlatformTransactionManager appropriata per indirizzare le transazioni attorno alle chiamate di metodi.

+0

Sono in grado di immaginare l'implementazione che '@Transactional (propagation = Propagation.NEVER)' protegge solo il metodo di lunga durata (ad esempio l'elaborazione IO) per essere chiamato dal contesto transazionale (senza effetti collaterali aggiuntivi - creazione di sessioni). Vedi i log di traccia nel mio post. – snieguu

+0

I registri illustrano esattamente ciò che affermano le risposte - che il comportamento è corretto. Se rimuovi '@Transactional (propagation = Propagation.NEVER)' significa che non verrà creata alcuna transazione, quindi ogni chiamata DAO risulterà nella creazione di una nuova sessione, che è esattamente ciò che accade nel tuo caso, come verificato dai log –

+0

Come per il tuo esempio: se lavori solo su IO, perché dovresti aver bisogno di transazioni di database? Quale lavoro correlato al DB si sta eseguendo? :) –

2

@Transactional (propagation = Propagation.NEVER) crea comunque una sessione. Se si utilizza la combinazione Spring/Hibernate/JPA per le transazioni non distribuite, si utilizza sicuramente lo 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 infrastrutture transazione primavera è: -

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à (sottostante implementazione è seduta in ibernazione), come si può vedere e questo è la prima cosa che questo metodo non

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

quindi sicuramente un'entità manager/si crea sessione. Gli attributi di transazione vengono poi passati al JpaDialect sottostante (HibernateJpaDialect) tramite TransactionDefinition. questa classe a sua volta ottiene in realtà sottostante Hibernate Session e la transazione API della sessione

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

Non penso che questo sia un comportamento corretto. È vero quello che i colleghi dicono che anche senza transazione l'ibernazione sta creando una sessione. Ma questo significa che ci troviamo di fronte a due sessioni S1 e S2 per le due letture separate dal DAO. Allo stesso tempo, la cache L1 è sempre per sessione, quindi non ha senso che due sessioni separate abbiano un hit per la cache L1. Sembra che la tua primavera non stia rispettando la @Transactional (propagation = Propagation.NEVER)

Il @Transactional (propagation = Propagation.NEVER) dovrebbe essere equivalente a se si inizializza il servizio da un metodo principale e si fa il successivo chiama tu stesso DAO.

Provalo in una classe principale e guarda come reagirà. Dubito che colpirà di nuovo la cache L1.

Anche io copiare incollare il documento da Sprint sulla propagazione MAI:

MAI Execute non transazionale, lanciare un'eccezione se una transazione esiste.

Un'altra domanda: l'ibernazione è configurata per AutoCommit. È possibile che "runInTransaction" - il metodo non stia commettendo?

+1

Semplicemente non è vero. Innanzitutto, è possibile visualizzare nei registri che nessuna transazione è stata avviata per "withoutTransaction", quindi non vedo le due transazioni di cui si sta parlando.Ci saranno due sessioni - una nel bean esterno, una nel bean interno - e ognuna di quelle sessioni avrà una propria cache L1. –

+1

'@Transactional (propagation = Propagation.NEVER)' dice a Spring che il proprio codice è * supposto * di interagire con e essere gestito con un gestore transazioni configurato e quindi descrive il tipo di interazione, ovvero che nessuna transazione deve essere avviata e che questo metodo non dovrebbe essere consentito per eseguire all'interno di un altro metodo che ha una propria transazione. Se non vuoi che quel tipo di comportamento ometti l'annotazione e otterrai il comportamento che hai descritto. –

+0

Spetta a te in qualità di sviluppatore sapere quale gestore di transazioni hai configurato e quale sarà l'interazione con il gestore delle transazioni. Se leggi i documenti per JpaTransactionManager, vedrai che il comportamento osservato è corretto –

2

Prima di tutto, quando si utilizza l'ibernazione dietro l'API JPA, userò il termine EntityManager anziché la 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.

In teoria il ciclo di vita dell'EM è breve e legato a un'unità di lavoro (e quindi in genere a una transazione, vedere Struggling to understand EntityManager proper use).

Ora JPA può essere utilizzato 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 allo TransactionManager (l'oggetto che gestisce le annotazioni @Transactional).

Quando si annota un metodo che utilizza @Transactional(propagation = Propagation.NEVER), si crea un ambito di transazione logica molla che assicurino che non sono transazioni JDBC sottostante esistente vincolato ad un eventuale EM esistente, che non creerà uno e utilizzerà la modalità autocommit JDBC ma che creerà un EM per questo ambito di transazione logico se nessuno esiste già.

Riguardo al fatto che una nuova istanza EM viene creata per ciascuna chiamata DAO quando non è definito alcun ambito logico di transazione, è necessario ricordare che non è possibile accedere al database utilizzando JPA al di fuori dell'EM. In questo caso, l'AFAIK Hibernate generava un errore 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 esiste alcun ambito logico di chiusura. 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.

+1

thx per la formazione :) – Gab

Problemi correlati