Condividi tramite


Utilizzo delle transazioni

Nota

Solo EF6 e versioni successive: funzionalità, API e altri argomenti discussi in questa pagina sono stati introdotti in Entity Framework 6. Se si usa una versione precedente, le informazioni qui riportate, o parte di esse, non sono applicabili.

Questo documento descrive l'uso delle transazioni in EF6, inclusi i miglioramenti aggiunti da EF5 per semplificare l'uso delle transazioni.

Operazioni di Entity Framework per impostazione predefinita

In tutte le versioni di Entity Framework, ogni volta che si esegue SaveChanges() per inserire, aggiornare o eliminare nel database il framework eseguirà il wrapping di tale operazione in una transazione. Questa transazione dura solo per un periodo di tempo sufficiente per eseguire l'operazione e quindi viene completata. Quando si esegue un'altra operazione di questo tipo viene avviata una nuova transazione.

A partire da EF6 Database.ExecuteSqlCommand() per impostazione predefinita eseguirà il wrapping del comando in una transazione se non ne era già presente uno. Esistono overload di questo metodo che consentono di eseguire l'override di questo comportamento, se lo si desidera. Anche nell'esecuzione di EF6 di stored procedure incluse nel modello tramite API come ObjectContext.ExecuteFunction() funziona allo stesso modo (ad eccezione del fatto che al momento non è possibile eseguire l'override del comportamento predefinito).

In entrambi i casi, il livello di isolamento della transazione è indipendentemente dal livello di isolamento che il provider di database considera l'impostazione predefinita. Per impostazione predefinita, ad esempio, in SQL Server si tratta di READ COMMITTED.

Entity Framework non esegue il wrapping delle query in una transazione.

Questa funzionalità predefinita è adatta a molti utenti e, in tal caso, non è necessario eseguire alcuna operazione diversa in EF6; scrivere semplicemente il codice come hai sempre fatto.

Tuttavia, alcuni utenti richiedono un maggiore controllo sulle transazioni, come illustrato nelle sezioni seguenti.

Funzionamento delle API

Prima di EF6 Entity Framework insistette sull'apertura della connessione al database stessa (generava un'eccezione se è stata passata una connessione già aperta). Poiché una transazione può essere avviata solo in una connessione aperta, ciò significava che l'unico modo in cui un utente poteva eseguire il wrapping di diverse operazioni in una transazione era quello di usare TransactionScope o di utilizzare ObjectContext.ConnessioneProprietà ion e iniziare a chiamare Open() e BeginTransaction() direttamente nell'oggetto Entity Connessione ion restituito. Inoltre, le chiamate API che hanno contattato il database non riuscirebbero se fosse stata avviata una transazione sulla connessione di database sottostante autonomamente.

Nota

La limitazione dell'accettazione delle connessioni chiuse è stata rimossa in Entity Framework 6. Per informazioni dettagliate, vedere gestione Connessione ion.

A partire da EF6, il framework offre ora:

  1. Database.BeginTransaction(): metodo più semplice per consentire a un utente di avviare e completare le transazioni stesse all'interno di un Oggetto DbContext esistente, consentendo di combinare diverse operazioni all'interno della stessa transazione e di conseguenza eseguire il commit o tutto il rollback come uno. Consente inoltre all'utente di specificare più facilmente il livello di isolamento per la transazione.
  2. Database.UseTransaction(): che consente a DbContext di usare una transazione avviata all'esterno di Entity Framework.

Combinazione di più operazioni in una transazione nello stesso contesto

Database.BeginTransaction() ha due override, uno che accetta un isolationLevel esplicito e uno che non accetta argomenti e usa l'elemento IsolationLevel predefinito dal provider di database sottostante. Entrambe le sostituzioni restituiscono un oggetto DbContextTransaction che fornisce metodi Commit() e Rollback() che eseguono il commit e il rollback nella transazione dell'archivio sottostante.

DbContextTransaction deve essere eliminato dopo il commit o il rollback. Un modo semplice per eseguire questa operazione è la sintassi using(...) {...} che chiamerà automaticamente Dispose() al termine del blocco using:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void StartOwnTransactionWithinContext()
        {
            using (var context = new BloggingContext())
            {
                using (var dbContextTransaction = context.Database.BeginTransaction())
                {
                    context.Database.ExecuteSqlCommand(
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'"
                        );

                    var query = context.Posts.Where(p => p.Blog.Rating >= 5);
                    foreach (var post in query)
                    {
                        post.Title += "[Cool Blog]";
                    }

                    context.SaveChanges();

                    dbContextTransaction.Commit();
                }
            }
        }
    }
}

Nota

Per avviare una transazione è necessario che la connessione all'archivio sottostante sia aperta. La chiamata a Database.BeginTransaction() aprirà quindi la connessione se non è già aperta. Se DbContextTransaction ha aperto la connessione, la chiuderà quando viene chiamato Dispose().

Passaggio di una transazione esistente al contesto

A volte si vuole una transazione ancora più ampia nell'ambito e che include operazioni sullo stesso database, ma al di fuori di EF completamente. A tale scopo, è necessario aprire la connessione e avviare la transazione manualmente e indicare a EF a) di usare la connessione al database già aperta e b) per usare la transazione esistente in tale connessione.

A tale scopo, è necessario definire e usare un costruttore nella classe di contesto che eredita da uno dei costruttori DbContext che accettano i) un parametro di connessione esistente e ii) contextOwns Connessione ion boolean.

Nota

Il flag contextOwns Connessione ion deve essere impostato su false quando viene chiamato in questo scenario. Questo aspetto è importante perché informa Entity Framework che non deve chiudere la connessione al momento dell'operazione (ad esempio, vedere la riga 4 seguente):

using (var conn = new SqlConnection("..."))
{
    conn.Open();
    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
    {
    }
}

Inoltre, è necessario avviare la transazione manualmente (incluso IsolationLevel se si vuole evitare l'impostazione predefinita) e informare Entity Framework che è già stata avviata una transazione esistente nella connessione (vedere la riga 33 seguente).

È quindi possibile eseguire operazioni di database direttamente in Sql Connessione ion o in DbContext. Tutte queste operazioni vengono eseguite all'interno di una transazione. Si assume la responsabilità di eseguire il commit o il rollback della transazione e di chiamare Dispose(), nonché di chiudere e eliminare la connessione al database. Ad esempio:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
     class TransactionsExample
     {
        static void UsingExternalTransaction()
        {
            using (var conn = new SqlConnection("..."))
            {
               conn.Open();

               using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot))
               {
                   var sqlCommand = new SqlCommand();
                   sqlCommand.Connection = conn;
                   sqlCommand.Transaction = sqlTxn;
                   sqlCommand.CommandText =
                       @"UPDATE Blogs SET Rating = 5" +
                        " WHERE Name LIKE '%Entity Framework%'";
                   sqlCommand.ExecuteNonQuery();

                   using (var context =  
                     new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        context.Database.UseTransaction(sqlTxn);

                        var query =  context.Posts.Where(p => p.Blog.Rating >= 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }
                       context.SaveChanges();
                    }

                    sqlTxn.Commit();
                }
            }
        }
    }
}

Cancellazione della transazione

È possibile passare null a Database.UseTransaction() per cancellare le conoscenze di Entity Framework della transazione corrente. Entity Framework non eseguirà né eseguirà il commit né il rollback della transazione esistente quando si esegue questa operazione, quindi usare con attenzione e solo se si è certi che si tratta di ciò che si vuole fare.

Errori in UseTransaction

Se si passa una transazione quando si passa una transazione, verrà visualizzata un'eccezione da Database.UseTransaction():

  • Entity Framework ha già una transazione esistente
  • Entity Framework è già operativo all'interno di TransactionScope
  • L'oggetto connessione nella transazione passata è Null. Ovvero, la transazione non è associata a una connessione, in genere si tratta di un segno che la transazione è già stata completata
  • L'oggetto connessione nella transazione passata non corrisponde alla connessione di Entity Framework.

Uso delle transazioni con altre funzionalità

Questa sezione descrive in dettaglio il modo in cui le transazioni precedenti interagiscono con:

  • Resilienza della connessione
  • Metodi asincroni
  • Transazioni TransactionScope

Resilienza della connessione

La nuova funzionalità di resilienza di Connessione non funziona con le transazioni avviate dall'utente. Per informazioni dettagliate, vedere Ripetizione delle strategie di esecuzione.

Programmazione asincrona

L'approccio descritto nelle sezioni precedenti non richiede altre opzioni o impostazioni per lavorare con la query asincrona e i metodi di salvataggio. Tenere tuttavia presente che, a seconda delle operazioni eseguite all'interno dei metodi asincroni, ciò può comportare transazioni a esecuzione prolungata, che a sua volta possono causare deadlock o blocchi che non sono ottimali per le prestazioni dell'applicazione complessiva.

Transazioni TransactionScope

Prima di EF6, il modo consigliato per fornire transazioni di ambito più grandi consiste nell'usare un oggetto TransactionScope:

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void UsingTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required))
            {
                using (var conn = new SqlConnection("..."))
                {
                    conn.Open();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    sqlCommand.ExecuteNonQuery();

                    using (var context =
                        new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }
                        context.SaveChanges();
                    }
                }

                scope.Complete();
            }
        }
    }
}

Sql Connessione ion ed Entity Framework usano entrambe la transazione TransactionScope di ambiente e quindi viene eseguito il commit insieme.

A partire da .NET 4.5.1 TransactionScope è stato aggiornato per funzionare anche con metodi asincroni tramite l'uso dell'enumerazione TransactionScopeAsyncFlowOption :

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        public static void AsyncTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
            {
                using (var conn = new SqlConnection("..."))
                {
                    await conn.OpenAsync();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    await sqlCommand.ExecuteNonQueryAsync();

                    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }

                        await context.SaveChangesAsync();
                    }
                }
                
                scope.Complete();
            }
        }
    }
}

Esistono ancora alcune limitazioni per l'approccio TransactionScope:

  • Richiede .NET 4.5.1 o versione successiva per lavorare con metodi asincroni.
  • Non può essere usato negli scenari cloud, a meno che non si sia certi di avere una sola connessione (gli scenari cloud non supportano le transazioni distribuite).
  • Non può essere combinato con l'approccio Database.UseTransaction() delle sezioni precedenti.
  • Genererà eccezioni se si rilascia un DDL e non sono state abilitate le transazioni distribuite tramite il servizio MSDTC.

Vantaggi dell'approccio TransactionScope:

  • Aggiornerà automaticamente una transazione locale a una transazione distribuita se si effettua più di una connessione a un determinato database o si combina una connessione a un database con una connessione a un database diverso all'interno della stessa transazione . Si noti che il servizio MSDTC è configurato per consentire il funzionamento delle transazioni distribuite.
  • Facilità di codifica. Se si preferisce che la transazione sia ambientale e gestita in modo implicito in background anziché in modo esplicito sotto il controllo, l'approccio TransactionScope può essere più adatto.

In sintesi, con le nuove API Database.BeginTransaction() e Database.UseTransaction() precedenti, l'approccio TransactionScope non è più necessario per la maggior parte degli utenti. Se si continua a usare TransactionScope, tenere presente le limitazioni precedenti. È consigliabile usare l'approccio descritto nelle sezioni precedenti, se possibile.