Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

di il
19 risposte

Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

Anni fa avevo letto un articolo molto interessante ed esaustivo che avevo anche compreso bene e che ora però non riesco più a trovare e ricordo vagamente ma che credo fosse relativo all'utilità del codice "BEGIN TRANSACTION ... COMMIT". Vi spiego per sommi capi. Era un articolo che parlava di come progettare le query verso un DB di un e-commerce. In pratica si faceva questo esempio che io riporto molto velocemente:
Paolo vuole comprare il prodotto A, quindi verifica che nel DB vi sia presente un quantitativo del prodotto A sufficiente (query 1);
Se il prodotto è disponibile lo acquista (query 2);
Se il prodotto è stato acquistato il quantitativo del prodotto A deve essere ridotto di un'unità (query 3);
Se queste query fossero indipendenti ci sarebbe il rischio che la query 2 venga eseguita prima della query 3 di un altro utente e che venga quindi acquistato un prodotto inesistente.
Il discorso era grossolanamente questo ma lo ricordo per sommi capi.
Mi sembra di ricordare che la soluzione fosse appunto questa:
BEGIN TRANSACTION
Query 1
Query 2
Query 3
COMMIT
In sostanza "BEGIN TRANSACTION ... COMMIT" rende le 3 query un'unica operazione. Se la query 3 fallisce, il numero dei prodotti di un campo diventa negativo quindi contrario ad un preciso CHECK, l'effetto di tutte le query precedenti (query 1 e 2) viene annullato.
1) Quello che non ricordo più è se "BEGIN TRANSACTION ... COMMIT" è bloccante ovvero quando Paolo prova a cercare un prodotto può farlo se nessun altro utente ha già fatto altrove la stessa operazione.
Ovviamente non sono sicuro che fosse proprio questa la soluzione e non escludo che ci fosse dell'altro.
2) Prima di tutto vi chiedo una conferma su questo.
Nella mia applicazione alcune @Servlet eseguono molte query (se quel campo è così fai cosà, se quella quantità li è maggiore di quell'altra procedi a fare quel tipo di operazione, ecc…) e progettando queste classi io non mi sono minimamente preoccupato di questo "BEGIN TRANSACTION ... COMMIT" però credo di aver fatto un grosso errore ed ora vorrei ricorrere ai ripari.
Tutte le classi query (se non tutte il 99%) hanno praticamente un solo metodo che in base ai dati prelevati dal controller eseguono un certo tipo di operazioni:
@Service
public class RiempiTabelle {
    public void riempi(OggettoForm oggettoForm){
	// query 1
	// …
	// query N
    }
}
Io sto valutando di editare queste classi in questo modo:
@Service
public class RiempiTabelle {

    public void riempi(OggettoForm oggettoForm){
        unaTabellaACasoRepository.eseguiBeginTransaction();
        riempiNonAtomico(oggettoForm);
        unaTabellaACasoRepository.eseguiCommit();
    }

    public void riempiNonAtomico(OggettoForm oggettoForm){
        // query 1
        // …
        // query N
    }

}
3) E' corretto questo tipo di approccio, esiste un'annotazione apposita che mi permette di evitare di fare tutto questo oppure ho le idee confuse?
Ricordo anche che l'effetto di questo "BEGIN TRANSACTION ... COMMIT" o dell'approccio al problema comporta un appesantimento dell'intera applicazione pertanto bisogna non abusare del codice evitando per esempio di utilizzare il codice a quelle query che eseguono solo un'operazione di lettura.
4) Concordate anche su questo ultimo punto?

19 Risposte

  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    Ciao

    Il problema della concorrenza sui database è abbastanza diffusa e ci sono un sacco di post/tutorial a riguardo.
    Come spesso accade in programmazione, la risposta è "dipende".

    Nel tuo ultimo esempio c'è un errore di fondo: la transazione è a livello di connessione, non di tabella. Pertanto quando fai una BEGIN, tutte le query fatte dopo entrano a far parte della transazione. Quando fai la COMMIT (o la ROLLBACK), confermi o annulli tutte le operazioni fatte.

    Quanto al lock, dipende sempre dal tipo di concorrenza.
    Spiegare tutto in un post è difficile, però se cerchi in giro la gestione della concorrenza coi database dovresti trovarla.
  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    La gestione delle transazione e' un aspetto FONDAMENTALE che uno DEVE conoscere quando 'traffica' con i database.

    Non conoscerla equivale ad andare in giro in automobile SENZA MAI controllare il livello dell'olio: funziona tutto bene finche' non grippi
    Dopo di che sono 'cavoli amari'

    Raramente si usano ESPLICITAMENTE le transazioni con le select.
    MA SI USANO con INSERT/UPDATE.

    Anche se tu NON USI ESPLICITAMENTE la transazione, questa viene SEMPRE (NEL 100% dei casi) USATA per accedere al database. In questo caso viene creata AUTOMATICAMENTE dal driver.

    La differenza e' che il driver crea una transazione PER OGNI SINGOLO statement SQL eseguito e CHE TERMINERA' SEMPRE con un COMMIT.

    Come conseguenza, il tuo codice SARA' MOOOOLTO PIU' LENTO che non usandole ESPLICITAMENTE: se devi inserire 1000.000 di record, farai 1000.000 di transazioni !

    Se poi la transazione sia bloccante o no DIPENDE da come viene creata: si puo' fare TUTTO: bloccante, non bloccante, a livello di tabella, di record, di collezione di tabelle o di record, ...

    Nota: questo non centra NIENTE con Java: sono funzionalita' STANDARD messe a disposizione da TUTTI i possibili tipi di DBMS, relazionali e non!
  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    Grazie ragazzi, perdonate l’ignoranza ma non sono un programmatore. Quindi ho visto giusto e quello che devo fare, correggetemi se sbaglio è rimuovere tutte le transazioni relativamente alle singole query e metterle a livello di @Servlet. Resta da capire se sia meglio una transazione bloccante oppure no ma se voglio che i processi di una @Servlet non si sovrappongano credo che mi convenga una transazione bloccante. L’unica cosa da capire è come fare questa cosa con jdbcTemplate. Comunque, non dovrebbe essere una cosa complicata. PiGi78 e Migliorabile voi che framework usate per gestire il DB? Lavorate anche con java oppure usate altro? Scommetto che voi usate gli ORM.
  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    iBaffiPro ha scritto:


    Io sto valutando di editare queste classi in questo modo:
        public void riempi(OggettoForm oggettoForm){
            unaTabellaACasoRepository.eseguiBeginTransaction();
            riempiNonAtomico(oggettoForm);
            unaTabellaACasoRepository.eseguiCommit();
        }
    
    No, primo perché è più prolisso, secondo perché non stai minimamente considerando le eccezioni e l'applicazione di un "rollback" nel caso di eccezione.

    iBaffiPro ha scritto:


    esiste un'annotazione apposita che mi permette di evitare di fare tutto questo
    In Spring esiste l'annotation @Transactional (org.springframework.transaction.annotation.Transactional). Per compatibilità è anche supportata l'annotation javax.transaction.Transactional di derivazione JavaEE (ma nota che ha caratteristiche e field differenti).

    La gestione delle transazioni in Spring Boot è automaticamente attiva e disponibile se c'è almeno lo starter spring-boot-starter-jdbc.

    Il @Transactional lo puoi mettere a livello di metodo o di classe. Ma attenzione, così come per il @Async che ti avevo già spiegato (e in generale la AOP, Aspect Oriented Programming, in Spring), il Transactional viene gestito tramite proxy.
    Questo significa che "funziona" solo per i metodi public e solo se il metodo lo invochi "dall'esterno" cioè sul reference che è iniettato altrove ad esempio con un @Autowired, perché quello iniettato non è il bean puro ma il proxy che ha tutta la "macchineria" per gestire la transazionalità.

    Se metti @Transactional su un metodo private, NON funziona. Se chiami anche dall'esterno un metodo non @Transactional ma poi dall'interno, dalla stessa classe, chiami un metodo anche public @Transactional (self-invocation), NON funziona.

    Poi c'è la questione delle eccezioni. Per default, le eccezioni "unchecked" causano un rollback mentre quelle "checked" non lo causano. Le regole sul rollback possono essere cambiate caso per caso specificando dei field sulla annotation @Transactional. E nota che il JdbcTemplate lancia solo eccezioni unchecked.

    Poi ci sarebbe tutta la questione sui concetti di propagation e isolation della transazione. Ma non è che ti posso dire tutto qui (sia perché lungo, sia perché non ricordo tutto a memoria). Dovresti leggere la documentazione di Spring ...
  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    Da quello che leggo qui:
    https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-declarative
    sembra che esista un’annotazione che tra il resto uso già nelle classi repository che si chiama @Transactional. Nelle classi repository uso l’annotazione in questo modo:
    @Transactional(readOnly=true)
    ed ovviamente solo per i metodi che effettuano una lettura del DBMS.
    Se rimuovo tutte le annotazioni dalle classi repository e metto sul metodo della classe @Transactional(readOnly=false) a vostro avviso risolvo? È questa la soluzione per il mio caso? Devo fare altro? Voi che tecnologia usate? Come vi comportate?
  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    andbin ha scritto:


    iBaffiPro ha scritto:


    Io sto valutando di editare queste classi in questo modo:
        public void riempi(OggettoForm oggettoForm){
            unaTabellaACasoRepository.eseguiBeginTransaction();
            riempiNonAtomico(oggettoForm);
            unaTabellaACasoRepository.eseguiCommit();
        }
    
    No, primo perché è più prolisso, secondo perché non stai minimamente considerando le eccezioni e l'applicazione di un "rollback" nel caso di eccezione.
    Immaginavo che ci fosse qualcosa di pronto.

    andbin ha scritto:


    In Spring esiste l'annotation @Transactional (org.springframework.transaction.annotation.Transactional). Per compatibilità è anche supportata l'annotation javax.transaction.Transactional di derivazione JavaEE (ma nota che ha caratteristiche e field differenti).
    Abbiamo postato praticamente insieme, avevo visto anche io questa soluzione.

    andbin ha scritto:


    La gestione delle transazioni in Spring Boot è automaticamente attiva e disponibile se c'è almeno lo starter spring-boot-starter-jdbc.
    Il @Transactional lo puoi mettere a livello di metodo o di classe. Ma attenzione, così come per il @Async che ti avevo già spiegato (e in generale la AOP, Aspect Oriented Programming, in Spring), il Transactional viene gestito tramite proxy.
    Questo significa che "funziona" solo per i metodi public e solo se il metodo lo invochi "dall'esterno" cioè sul reference che è iniettato altrove ad esempio con un @Autowired, perché quello iniettato non è il bean puro ma il proxy che ha tutta la "macchineria" per gestire la transazionalità.
    Se metti @Transactional su un metodo private, NON funziona. Se chiami anche dall'esterno un metodo non @Transactional ma poi dall'interno, dalla stessa classe, chiami un metodo anche public @Transactional (self-invocation), NON funziona.
    Qui per il mio caso non dovrebbero esserci problemi di nessun tipo perché io nel @Controller carico la classe @Service con @Autowired. Nel metodo con @RequestMapping(value = "/url-della-pagina", method = RequestMethod.GET/POST) scrivo:
    classeService.metodo(oggettoConIDatiDelForm);

    andbin ha scritto:


    iBaffiPro ha scritto:


    Io sto valutando di editare queste classi in questo modo:
        public void riempi(OggettoForm oggettoForm){
            unaTabellaACasoRepository.eseguiBeginTransaction();
            riempiNonAtomico(oggettoForm);
            unaTabellaACasoRepository.eseguiCommit();
        }
    
    No, primo perché è più prolisso, secondo perché non stai minimamente considerando le eccezioni e l'applicazione di un "rollback" nel caso di eccezione.
    Poi c'è la questione delle eccezioni. Per default, le eccezioni "unchecked" causano un rollback mentre quelle "checked" non lo causano. Le regole sul rollback possono essere cambiate caso per caso specificando dei field sulla annotation @Transactional. E nota che il JdbcTemplate lancia solo eccezioni unchecked.
    Se JdbcTemplate esegue un rollback per tutte le eccezioni unchecked direi che è buono ed è anche quello che desidero che avvenga. Le eccezioni le gestisco già a livello di @Service. In pratica tra le N query, se una di queste non va a buon fine me ne accorgo. Esempio banalissimo:
         
            @Service
            ...           
    	try{
    		utenteRegistrato = utenteRepository.inserisciUtente(utenteRegistrato);
    	}catch (Exception e){
    		utenteRegistrato = null;
    		erroreRegistrazione = "Registrazione non avvenuta!";
    	}
    	...
    	@Controller
    	...
    	model.addAttribute("ErroreRegistrazione", erroreRegistrazione);
    
    Secondo te bisogna aggiungere un'ulteriore eccezione per il rollback? Immagino che in questo modo si registri solo l'eccezzione di processoRiempi(), giusto?
         
    	@Transactional
    	public void riempi(OggettoForm oggettoForm){
    	try{
    		processoRiempi(oggettoForm); // il metodo che ora si chiama riempi() richiamato dal @Controller
    	}catch (Exception e){
    		erroreTransactional = "L'operazione che si desidera fare non è eseguita perché il DB era occupato!";
    	}
        }
    

    iBaffiPro ha scritto:


    Poi ci sarebbe tutta la questione sui concetti di propagation e isolation della transazione. Ma non è che ti posso dire tutto qui (sia perché lungo, sia perché non ricordo tutto a memoria). Dovresti leggere la documentazione di Spring ...
    http://javaliere.blogspot.com/2015/03/gestire-la-transazionalita-con-spring.html
    Qui c'è scritto qualcosa di utile in modo comprensibile.
  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    Anche qui c'è qualcosa di interessante:
    https://www.baeldung.com/transaction-configuration-with-jpa-and-spring
    Una cosa che non capisco è cosa accade mettendo solo @Transactional senza attributi quando si verifica un conflitto (l’utente A vuole acquistare un prodotto P nello stesso istante che l’utente B lo vuole comperare). Una delle due transazioni viene bloccata oppure si accoda ed attende che il database ritorni disponibile?
    A mio avviso sarebbe buona cosa che A oppure B attendano il completamento della transazione della controparte per un periodo di tempo T e poi eseguano la propria. Se si supera un certo tempo T si invia un’eccezione ad A oppure a B (“il DB era occupato, ritenta in un secondo momento…”).
    Andrea, tu come ti comporti nelle tue applicazioni a livello di transazioni? Sulla base della tua esperienza che approccio ritieni più prudente utilizzare?
  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    iBaffiPro ha scritto:


    Una cosa che non capisco è cosa accade mettendo solo @Transactional senza attributi quando si verifica un conflitto (l’utente A vuole acquistare un prodotto P nello stesso istante che l’utente B lo vuole comperare). Una delle due transazioni viene bloccata oppure si accoda ed attende che il database ritorni disponibile?
    Allora, chiariamo una cosa: la transazione NON è un lock. Forse di recente hai visto cose tipo synchronized, ReentrantLock, ecc... Questi sono dei lock, c'è proprio una "mutua-esclusione" a livello di thread che impedisce al thread B di acquisire il lock (e quindi continuare) se il lock l'ha già acquisito il thread A.

    Come ho detto, la transazione NON è un lock! Se c'è un metodo @Transactional salva(), due thread possono benissimo entrarci in maniera concorrente. Non si blocca nulla a livello di thread. E a questo punto stai pensando: ma così non succedono casini? DIPENDE. Cioè dipende da cosa viene fatto nella transazione. Ci sono molti scenari che NON danno alcun problema.

    Se due transazioni concorrenti fanno solo SELECT, non c'è alcun problema. Se due transazioni concorrenti fanno INSERT (anche sulla stessa tabella), non c'è alcun problema. Se due transazioni fanno UPDATE, anche sulla stessa tabella ma record differenti, idem non c'è alcun problema. Ecc...

    E' chiaro che ci sono casi particolari e più critici. Tipo:

    Che succede se nella transazione A si inserisce un record e poco dopo (mentre A è ancora in corso) una transazione B fa una select che per il criterio potrebbe tirar fuori quel record inserito? B "vede" quel nuovo record oppure no? DIPENDE dal isolation level.

    Altro caso:
                   thread X                       thread Y
                insertPerson()               deleteAllPersons()
     tempo
       |
       |   ---start transaction A---         
       |                                  ---start transaction B---
       |    INSERT INTO persone (...
       |                                    DELETE FROM persone
       |   ---end transaction A---
       |                                  ---end transaction B---
       v
    Il DELETE fatto appena dopo nella transazione B "vede" (e quindi elimina) il record inserito nella transazione A? Vediamo se hai compreso: dipende dal ....... isolation level!


    Se non metti attributi al @Transactional, ha un Isolation.DEFAULT che in sostanza vuol dire: dipende dal datastore sottostante (in primis il connection-pool).

    HikariCP (il connection pool usato da Spring Boot) ha una proprietà transactionIsolation che per default non indica nulla e si basa quindi sul isolation del driver JDBC.

    Nel driver JDBC di PostgreSQL se non sbaglio (non sono sicuro al 100%) per default dipende dal isolation predefinito del server PostgreSQL, che è Read Committed. Questo perlomeno è specificato qui: https://www.postgresql.org/docs/current/transaction-iso.html
    "Read Committed is the default isolation level in PostgreSQL."

    Per concludere: le transazioni servono principalmente per garantire quel comportamento che si dice "o tutto o niente". Ovvero: o deve risultare che tutte le operazioni sono state fatte ed applicate, o se succede qualcosa di errato in mezzo deve risultare come se nessuna delle operazioni sia stata fatta.
    Ma le transazioni non sono da considerare come una "mutua esclusione" (non a quel livello tra thread che forse stavi pensando).

    Se poi la questione è capire a livello molto "fine" cosa succede a livello di "visibilità" dei dati tra transazioni, questo dipende dal isolation level.
  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    Fantastico Andrea!
    Mi hai fatto capire tante cose con questo post! Grazie di cuore. Devo aggiungere un testo sui DB oltre che di Java.
    Per il discorso dell'isolation sto riflettendo sul comportamento da prediligere ma credo che nel mio caso mi convenga scegliere l'opzione secondo cui le transazioni non restino isolate: si cancella il record anche se la transazione di inserimento non è terminata. Credo comunque che entrambe le opzioni possano andare bene e non compromettano il funzionamento della mia applicazione.
    Ti chiedo ancora alcune cosette:
    1) Nell'esempio che hai postato, dovrei settare il tipo di isolation sulla transazione B, giusto?
    2) Se cancello il record prima che la transazione A termini, tralasciando il discorso consistenza del DB, la tabella ruoli non viene editata perché la transazione A prima di chiudersi si accorge che in 'persone' manca il record quindi annulla tutti gli effetti della transazione, giusto?
    
                   thread X                       thread Y
                insertPerson()               deleteAllPersons()
     tempo
       |
       |   ---start transaction A---         
       |                                  ---start transaction B---
       |    INSERT INTO persone (...
       |                                    DELETE FROM persone
       |    INSERT INTO ruoli (...
       |   ---end transaction A---
       |                                  ---end transaction B---
       v
    
    3) L'annotazione @Transactional riduce le performance ma anche il numero di query verso il database, giusto? Se per eseguire una data transazione uso 10 query invece che 20 impiego metà del tempo, giusto? Nell'esempio sotto il CASO 1 impiega il doppio del tempo del caso 2, giusto?
    CASO 1
    
                   thread X                   
                insertPerson()               
     tempo
       |   ---start transaction A---         
       |    SELECT * FROM utenti WHERE nome=Paolo
       |    INSERT INTO utenti (nome, password) VALUES ('Paolo', 'Password')
       |   ---end transaction A---
       v
    
    CASO 2
    
                   thread X                   
                insertPerson()               
     tempo
       |   ---start transaction A---         
       |    INSERT INTO utenti (nome, password) VALUES ('Paolo', 'Password')
       |   ---end transaction A---
       v
    
    4) Leggo che 'propagation' specifica il comportamento nel caso in cui un metodo transazionale viene eseguito quando esiste già un contesto di transazione ma non capisco quale casistica dovrebbe regolamentare. Riesci a farmi un esempio che mostra l'utilità anche di questo 'propagation'?
  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    Ok, da quanto hai appena risposto, deduco che qualcosa lo stai comprendendo. Ma forse è meglio se fornisco qualche spunto in più.

    Se vai a leggere della documentazione sul isolation level nelle transazioni su DB (anche banalmente su ), vedrai che ci sono 3 tipi di "anomalie" che possono avvenire. Sono denominate:
    - Dirty reads
    - Non-repeatable reads
    - Phantom reads

    Provo a farti un esempio ideato al volo di dirty reads che è abbastanza semplice ma eloquente:
    tempo
      |    ---start transazione A---
      |     INSERT INTO libri (.....
      |                                  ---start transazione B---
      |                                  SELECT * FROM libri
      |     INSERT INTO autori (.....    ---end transazione B---
      |     INSERT INTO autori (.....
      |    ---end transazione A---
      v
    A livello di codice immagina di avere un LibriService con un metodo @Transactional inserisciLibro(Libro) di questo tipo (abbozzato ma per dare l'idea):
    @Transactional
    public void inserisciLibro(Libro libro) {
        libroDao.insert(libro);
        for (Autore autore : libro.getAutori()) {
            autoreDao.insert(autore);
        }
    }
    E immagina che venga inserito un libro con 2 autori come mostrato nella transazione A. La domanda ora è: la transazione B in cui legge TUTTI i libri, "vede" il libro inserito nella transazione A?? Nota che la SELECT è fatta poco dopo la prima INSERT e comunque prima della fine di transazione A.

    Ora immagina che B "vede" il libro inserito. Potrebbe però capitare che la seconda INSERT dell'autore fallisce. Per qualunque motivo possibile (vincolo violato, ecc...), non ha importanza ora perché fallisce. Ma se fallisce, essendo in una transazione, deve avvenire un rollback, quindi su database quel libro di fatto NON viene reso persistente e materialmente NON "esiste".

    Ma la SELECT aveva visto e letto quel libro! Quindi se la query è fatta per presentare i libri all'utente, l'utente B vede nella sua pagina web un libro che NON esiste!! Bene, questo si chiama appunto un dirty read, una lettura "sporca" dovuta al fatto che la transazione B ha "visto" dei dati non ancora committati da un'altra transazione (la A) e che poi a causa del rollback sono spariti.

    Il livello di isolamento chiamato Read Uncommitted permette i dirty reads mentre invece l'isolamento chiamato Read Committed non li permette.
    Nota che in PostgreSQL il Read Uncommitted non è gestito e, se settato, si comporta come Read Committed. Questo significa che perlomeno in PostgreSQL i dirty reads non sono mai possibili. Su altri database invece il Read Uncommitted è gestito e settabile.

    Lo dice la documentazione https://www.postgresql.org/docs/current/sql-set-transaction.html
    The SQL standard defines one additional level, READ UNCOMMITTED. In PostgreSQL READ UNCOMMITTED is treated as READ COMMITTED.

    Quindi devi conoscere queste 3 anomalie, Dirty reads/Non-repeatable reads/Phantom reads, e in base a cosa devi fare devi determinare se e come queste anomalie ti possono dare problemi. E di conseguenza settare l'isolation level o globalmente per ogni Connection oppure per-transaction.

    L'isolamento "peggiore" è il Read Uncommitted mentre quello migliore e più consistente è il Serializable. Per garantire maggiore isolamento il database (quindi NON la tua applicazione) va ad utilizzare internamente una serie di lock sulla riga o su range di righe o addirittura sulla intera tabella.
    Quindi come ho già detto prima, NON è la invocazione del tuo metodo inserisciLibro() che si blocca e non ci entra dentro. Sono invece le singole query che ci possono mettere più tempo perché ci sono dei lock tenuti dal db a causa di un'altra transazione. Questo significa pertanto minore scalabilità e minore performance.
  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    Ho letto anche le altre due e credo che mi convenga puntare su serializable perché come scrivi tu è la soluzione più consistente. Di certo devo togliere i @Transactional(readOnly=true) dai metodi delle classi @Repository e metterli sulle @Service con attributi differenti così da creare una transazione con isolation serializable a livello di @Service. Io ho una servet per la registrazione, una per il login, l'altra per modificare il nome utente, una per l'inserimento di dati prelevati da un form, una per leggere dati da varie tabelle e presentarli ad una pagina web, ecc...
    Per quanto riguarda la propagazione esistono 2 sole opzioni REQUIRED e REQUIRES_NEW. Se ho capito bene Spring Boot usa REQUIRED che dovrebbe essere quella più consistente. In pratica se ho 2 transazioni, una dentro l'altra, se quella interna non fallisce e quella esterna si, possono generarsi delle incoerenze. Con REQUIRED se la transazione esterna fallisce, quella interna viene fatta comunque fallire ovvero su essa viene fatto il rollback.
    Altra cosa che non mi piace è non specificare questi attributi. Di default readonly è su false ma se con Spring Boot 4 o 5 questa specifica dovesse cambiare? Anche a livello di leggibilità del codice è meglio che questi termini compaiano, soprattutto per me che sono alle prime armi, così mi ricordo il tipo di transazione che ho deciso di fare in quel momento.
    Quindi in sostanza, pensavo a queste 2 transazioni:
    @Transactional(isolation = Isolation.ISOLATION_SERIALIZABLE, propagation = Propagation.REQUIRED, rollbackFor = Exception.class, readOnly=false)
    @Transactional(isolation = Isolation.ISOLATION_SERIALIZABLE, propagation = Propagation.REQUIRED, rollbackFor = Exception.class, readOnly=true)
    L'idea è di rimuovere @Transactional(readOnly=true) da @Repository e mettere la seconda opzione tra quelle elencate sopra su tutte le @Service che contengono esclusivamente query di tipo SELECT e la prima su tutte le altre. Pensavo di mettere l'annotazione a livello di metodo e non di classe. A livello di codice non cambia nulla, Spring Boot le accetta entrambe ma ti chiedo anche conferma su questo.
  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    iBaffiPro ha scritto:


    Ho letto anche le altre due e credo che mi convenga puntare su serializable perché come scrivi tu è la soluzione più consistente.
    Sì ma il peggiore in termini di concorrenza/performance. Il Serializable comunque NON andrebbe mai messo globale (sulla Connection/connection-pool intendo). PostgreSQL per default usa Read Committed, il MySQL se non sbaglio ha come default Repeatable Read (che è un pelino meglio di Read Committed) e l'Oracle DB ha come default il Read Committed. Ma che io sappia NESSUN database usa Serializable come default perché causerebbe delle prestazioni generali orribili.

    Quindi Serializable al massimo va usato solo per certe transazioni che ritieni "critiche" riguardo la consistenza. E ovviamente si presuppone che hai CAPITO molto bene le "anomalie" che possono avvenire (quelle citate prima).

    iBaffiPro ha scritto:


    Di certo devo togliere i @Transactional(readOnly=true) dai metodi delle classi @Repository
    Il @Transactional non va messo sui Dao/Repository!! (parlo di Dao/Repository sostanzialmente come la stessa cosa, dipende solo dalla tecnologia usata e dal modo di implementazione).
    I metodi di un Dao/Repository generalmente rappresentano già le "primitive" minime di accesso al db, ovvero ciascun metodo di norma fa 1 sola query e basta.
    Sono i Service che hanno facoltà di usare più Dao/Repository (vedi esempio del libro/autori di prima) orchestrando più chiamate ai Dao/Repository secondo le necessità. E quindi è sui Service che ha senso ragionare sulla transazionalità ... non sui Dao/Repository.

    iBaffiPro ha scritto:


    Per quanto riguarda la propagazione esistono 2 sole opzioni REQUIRED e REQUIRES_NEW.
    No, non sono solo queste due. Ma:
  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    Serializable è usato dal database SQL di Microsoft ma in effetti si dice che è molto scarso in termini di performance. Se ho capito bene la differenza è questa:
    Read Committed: non garantisce che un dato letto continui ad essere presente oppure assente dopo il commit;
    Repeatable Read: garantisce che i record letti restino invariati dopo il commit ma gli altri record della tabella possono cambiare (in pratica si bloccano i record letti e non tutta la tabella);
    Serializable: garantisce che la tabella nella quale si legge il record resti invariata dopo il commit (in pratica si blocca tutta la tabella e non solo i record letti). E' la regola che garantisce maggiore sicurezza ma rallenta molto la WebApp perché blocca un'intera tabella e quindi tutte le transazioni su essa.
    Quello che mi stai dicendo e che dovrei dedicare più tempo ed attenzione a questo aspetto e scegliere @Transaction diverse per @Service diverse e non generalizzare come sto facendo? Giusto?
    Volendo entrare più nello specifico vedo che nella classe che uso per la registrazione eseguo 4 query. Forse ne potrei fare una sola ma ho fatto così e mi accontento perché non ho più tempo ad ottimizzare tutto altrimenti riscriverei l'intera App senza ombra di dubbio.
    A) Verifico che nel DB sia presente il nome utente;
    B) Inserisco il nome utente;
    C) Verifico la presenza del ruolo che desidero assegnare;
    D) Aggiungo il ruolo all'utente.
    Io pensavo a questa soluzione:
    
        @Transactional(
                isolation = Isolation.READ_COMMITTED,
                propagation = Propagation.REQUIRES_NEW,
                rollbackFor = Exception.class,
                readOnly=false
        )
    
    READ_COMMITTED non garantisce che arrivati al punto D il nome utente sia ancora disponibile ma la transazione in quel caso fallirebbe ugualmente perché nello schema è presente il vincolo di unicità del nome. E' vero che il messaggio che il client leggerebbe non sarebbe quello di utente già presente però non è un problema grave, al nuovo tentativo l'utente verrebbe correttamente avvisato.
    Ecco il database:
    
    CREATE TABLE IF NOT EXISTS utenti (
        id BIGSERIAL NOT NULL,
        nome VARCHAR(100) NOT NULL,
        password VARCHAR(255) NOT NULL,
        CONSTRAINT utenti_pk PRIMARY KEY(id),
        CONSTRAINT utenti_uk1 UNIQUE (nome)
    );
    
    CREATE TABLE IF NOT EXISTS ruoli (
        id BIGSERIAL NOT NULL,
        ruolo VARCHAR(100) NOT NULL,
        CONSTRAINT ruoli_pk PRIMARY KEY(id),
        CONSTRAINT ruoli_uk UNIQUE (ruolo),
        CONSTRAINT lunghezza_minima_ruolo CHECK (LENGTH(ruolo) >= 5 AND ruolo !~ ' ')
    );
    
    CREATE TABLE IF NOT EXISTS utenti_ruoli (
        id_utente BIGINT NOT NULL,
        id_ruolo BIGINT NOT NULL,
        CONSTRAINT utenti_ruoli_fk_utente FOREIGN KEY (id_utente) REFERENCES utenti (id) ON DELETE CASCADE,
        CONSTRAINT utenti_ruoli_fk_ruolo FOREIGN KEY (id_ruolo) REFERENCES ruoli (id) ON DELETE CASCADE,
        CONSTRAINT utenti_ruoli_pk PRIMARY KEY(id_utente, id_ruolo)
    );
    
    CREATE TABLE IF NOT EXISTS variabili_sistema (
        id BIGSERIAL NOT NULL,
        variabile VARCHAR(100) NOT NULL,
        valore VARCHAR(255) NOT NULL,
        CONSTRAINT variabili_sistema_pk PRIMARY KEY(id),
        CONSTRAINT variabili_sistema_uk UNIQUE (variabile)
    );
    
    CREATE TABLE IF NOT EXISTS client (
        id BIGSERIAL NOT NULL,
        ip VARCHAR(39) NOT NULL,
        data TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
        CONSTRAINT client_pk PRIMARY KEY(id),
        CONSTRAINT client_uk UNIQUE (ip)
    );
    
    CREATE TABLE IF NOT EXISTS visite (
        id SMALLINT NOT NULL,
        visite_totali BIGINT NOT NULL,
        visite_giornaliere BIGINT NOT NULL,
        CONSTRAINT visite_pk PRIMARY KEY(id),
        CONSTRAINT visite_id_unico CHECK (id <= 1)
    );
  • Re: Come si esegue "BEGIN TRANSACTION ... COMMIT" in jdbcTemplate?

    iBaffiPro ha scritto:


    Read Committed: non garantisce che un dato letto continui ad essere presente oppure assente dopo il commit;
    Repeatable Read: garantisce che i record letti restino invariati dopo il commit ma gli altri record della tabella possono cambiare
    Serializable: garantisce che la tabella nella quale si legge il record resti invariata dopo il commit
    NOO. Leggi bene le spiegazioni che trovi online sulle 3 "anomalie": Dirty reads, Non-repeatable reads e Phantom reads.
    Non stare, per ora, a guardare COSA viene usato (snapshot, lock, ecc...) per impedire queste anomalie, perché DBMS differenti potrebbero usare tecniche o approcci (leggermente) differenti.
    È importante invece capire COSA avviene in ciascuna anomalia. Poi banalmente c'è la tabellina che ti dice quali anomalie sono permesse e non permesse per ciascun livello di isolamento.

    P.S. ma il Dirty reads l'ho spiegato prima! .. e credo pure bene ....

    iBaffiPro ha scritto:


    Quello che mi stai dicendo e che dovrei dedicare più tempo ed attenzione a questo aspetto e scegliere @Transaction diverse per @Service diverse e non generalizzare come sto facendo? Giusto?
    Per me, puoi anche mettere Serializable su tutte le tue transazioni .... l'importante è che concludi queste esercitazioni prima possibile ...
Devi accedere o registrarti per scrivere nel forum
19 risposte