Obsługa transakcji

Uwaga / Notatka

Tylko od EF6 wzwyż — funkcje, interfejsy API itp. omówione na tej stronie zostały wprowadzone w Entity Framework 6. Jeśli używasz starszej wersji, niektóre lub wszystkie informacje nie mają zastosowania.

W tym dokumencie opisano używanie transakcji w programie EF6, w tym ulepszenia dodane od czasu ef5, aby ułatwić pracę z transakcjami.

Co ef robi domyślnie

We wszystkich wersjach programu Entity Framework za każdym razem, gdy wykonasz polecenie SaveChanges(), aby wstawić, zaktualizować lub usunąć bazę danych, struktura będzie opakowować tę operację w transakcji. Ta transakcja trwa tylko tyle, ile potrzeba na wykonanie operacji, a następnie jest kończona. Po wykonaniu innej takiej operacji zostanie uruchomiona nowa transakcja.

Począwszy od EF6 Database.ExecuteSqlCommand() domyślnie obejmuje polecenie transakcją, jeśli jeszcze jej nie było. Istnieją przeciążenia tej metody, które umożliwiają zastąpienie tego zachowania, jeśli sobie tego życzysz. Również w programie EF6 wykonywanie procedur składowanych zawartych w modelu za pośrednictwem interfejsów API, takich jak ObjectContext.ExecuteFunction(), działa tak samo (z wyjątkiem tego, że zachowanie domyślne nie może być obecnie zastępowane).

W każdym przypadku poziom izolacji transakcji jest taki, jaki poziom izolacji dostawca bazy danych uznaje za ustawienie domyślne. Domyślnie na przykład w programie SQL Server jest to ODCZYT ZATWIERDZONY.

Program Entity Framework nie opakowuje zapytań w transakcji.

Ta domyślna funkcja jest odpowiednia dla wielu użytkowników, a jeśli tak, nie ma potrzeby wykonywać żadnych innych czynności w programie EF6; wystarczy napisać kod tak, jak zawsze.

Jednak niektórzy użytkownicy wymagają większej kontroli nad transakcjami — opisano to w poniższych sekcjach.

Jak działają interfejsy API

Do wersji EF6 Entity Framework wymagał otwarcia bezpośredniego połączenia z bazą danych (generował wyjątek, jeśli przekazywano mu połączenie, które było już otwarte). Ponieważ transakcja może być uruchamiana tylko w otwartym połączeniu, oznaczało to, że jedynym sposobem, w jaki użytkownik może owinąć kilka operacji w jedną transakcję, było użycie właściwości TransactionScope lub użycie właściwości ObjectContext.Connection i rozpoczęcie wywoływania metod Open() i BeginTransaction() bezpośrednio na zwróconym obiekcie EntityConnection . Ponadto wywołania interfejsu API, które skontaktowały się z bazą danych, zakończyłyby się niepowodzeniem, jeśli transakcja została uruchomiona na bazowym połączeniu bazy danych samodzielnie.

Uwaga / Notatka

Ograniczenie akceptowania tylko zamkniętych połączeń zostało usunięte w programie Entity Framework 6. Aby uzyskać szczegółowe informacje, zobacz Zarządzanie połączeniami.

Począwszy od platformy EF6, platforma zapewnia teraz:

  1. Database.BeginTransaction(): Łatwiejsza metoda dla użytkownika na rozpoczynanie i samodzielne przeprowadzanie transakcji w ramach istniejącego DbContext, umożliwiająca łączenie kilku operacji w ramach tej samej transakcji, a tym samym wszystkie są zatwierdzane lub wycofywane jako jedna. Umożliwia również użytkownikowi łatwiejsze określenie poziomu izolacji dla transakcji.
  2. Database.UseTransaction() : który umożliwia usłudze DbContext użycie transakcji, która została uruchomiona poza programem Entity Framework.

Łączenie kilku operacji w jednej transakcji w tym samym kontekście

Funkcja Database.BeginTransaction() ma dwa przesłonięcia — jedno, które przyjmuje jawną wartość IsolationLevel i które nie przyjmuje żadnych argumentów i używa domyślnego elementu IsolationLevel od bazowego dostawcy bazy danych. Oba przesłonięcia zwracają obiekt DbContextTransaction , który udostępnia metody Commit() i Rollback(), które wykonują zatwierdzanie i wycofywanie w podstawowej transakcji magazynu.

Funkcja DbContextTransaction ma zostać usunięta po zatwierdzeniu lub wycofaniu. Jednym z prostych sposobów wykonania tej czynności jest użycie(...) {...} składni, która automatycznie wywoła metodę Dispose() po zakończeniu korzystania z bloku:

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

Uwaga / Notatka

Rozpoczęcie transakcji wymaga otwarcia podstawowego połączenia z bazą danych. Wywołanie metody Database.BeginTransaction() spowoduje otwarcie połączenia, jeśli nie zostało jeszcze otwarte. Jeśli polecenie DbContextTransaction otworzyło połączenie, zamknie je po wywołaniu funkcji Dispose().

Przekazywanie istniejącej transakcji do kontekstu

Czasami potrzebujesz transakcji, która jest jeszcze szersza w zakresie i obejmuje operacje na tej samej bazie danych, ale całkowicie poza EF. Aby to osiągnąć, należy samodzielnie otworzyć połączenie i uruchomić transakcję, a następnie poinformować EF, aby a) użył już otwartego połączenia bazy danych, i b) korzystał z istniejącej transakcji na tym połączeniu.

Aby to zrobić, należy zdefiniować i użyć konstruktora w klasie kontekstu, który dziedziczy z jednego z konstruktorów DbContext, które przyjmują: i) istniejący parametr połączenia oraz ii) wartość logiczną typu boolean contextOwnsConnection.

Uwaga / Notatka

Flaga contextOwnsConnection musi być ustawiona na wartość false, gdy jest wywoływana w tym scenariuszu. Jest to ważne, ponieważ informuje to Entity Framework, że po zakończeniu działania nie powinien zamykać połączenia (na przykład, zobacz wiersz 4 poniżej):

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

Ponadto musisz samodzielnie rozpocząć transakcję (w tym ustawić IsolationLevel, jeśli chcesz uniknąć ustawień domyślnych) i poinformować Entity Framework, że na tym połączeniu jest już rozpoczęta transakcja (zobacz wiersz 33 poniżej).

Następnie możesz wykonywać operacje bazy danych bezpośrednio w samym programie SqlConnection lub w obiekcie DbContext. Wszystkie takie operacje są wykonywane w ramach jednej transakcji. Ponosisz odpowiedzialność za zatwierdzanie lub wycofywanie transakcji oraz wywoływanie metody Dispose(), a także zamykanie i usuwanie połączenia z bazą danych. Przykład:

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

Czyszczenie transakcji

Możesz przekazać wartość null do elementu Database.UseTransaction(), aby wyczyścić wiedzę platformy Entity Framework na temat bieżącej transakcji. Program Entity Framework nie zatwierdzi ani nie wycofa istniejącej transakcji, więc użyj jej z ostrożnością i tylko wtedy, gdy masz pewność, że jest to, co chcesz zrobić.

Błędy w funkcji UseTransaction

Jeśli przekażesz transakcję, zobaczysz wyjątek wygenerowany przez metodę Database.UseTransaction() w następujących sytuacjach:

  • Program Entity Framework ma już istniejącą transakcję
  • Entity Framework już działa w ramach TransactionScope
  • Obiekt połączenia w przekazanej transakcji ma wartość null. Oznacza to, że transakcja nie jest skojarzona z połączeniem — zazwyczaj jest to znak, że transakcja została już ukończona
  • Obiekt połączenia w przekazanej transakcji jest niezgodny z połączeniem programu Entity Framework.

Używanie transakcji z innymi funkcjami

W tej sekcji opisano sposób interakcji powyższych transakcji z:

  • Odporność połączenia
  • Metody asynchroniczne
  • Transakcje TransactionScope

Odporność połączenia

Nowa funkcja odporności połączenia nie działa z transakcjami inicjowanymi przez użytkownika. Aby uzyskać szczegółowe informacje, zobacz Strategie ponawiania prób wykonania.

Programowanie asynchroniczne

Podejście opisane w poprzednich sekcjach nie wymaga dalszych opcji ani ustawień do pracy z asynchronicznymi metodami zapytań i zapisywania. Należy jednak pamiętać, że w zależności od tego, co robisz w metodach asynchronicznych, może to spowodować długotrwałe transakcje — co z kolei może prowadzić do zakleszczeń lub blokowania, co negatywnie wpływa na wydajność całej aplikacji.

TransactionScope Transakcje

Przed ef6 zalecanym sposobem zapewnienia większego zakresu transakcji było użycie obiektu 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();
            }
        }
    }
}

Zarówno SqlConnection, jak i Entity Framework używają otaczającej transakcji TransactionScope i dlatego zostaną zatwierdzone razem.

Począwszy od .NET 4.5.1, TransactionScope został zaktualizowany, aby działać z metodami asynchronicznymi za pomocą wyliczenia 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();
            }
        }
    }
}

Nadal istnieją pewne ograniczenia dotyczące podejścia TransactionScope:

  • Wymaga programu .NET 4.5.1 lub nowszego do pracy z metodami asynchronicznymi.
  • Nie można jej używać w scenariuszach w chmurze, chyba że masz jedno i tylko jedno połączenie (scenariusze chmury nie obsługują transakcji rozproszonych).
  • Nie można połączyć jej z podejściem Database.UseTransaction() w poprzednich sekcjach.
  • Spowoduje to zgłoszenie wyjątków, jeśli użyjesz jakiegokolwiek polecenia DDL i nie włączono obsługi transakcji rozproszonych za pośrednictwem usługi MSDTC.

Zalety podejścia TransactionScope:

  • Spowoduje to automatyczne uaktualnienie transakcji lokalnej do transakcji rozproszonej, jeśli wykonasz więcej niż jedno połączenie z daną bazą danych lub połączysz połączenie z jedną bazą danych z połączeniem z inną bazą danych w ramach tej samej transakcji (uwaga: musisz mieć usługę MSDTC skonfigurowaną tak, aby umożliwić wykonywanie transakcji rozproszonych).
  • Łatwość kodowania. Jeśli wolisz, aby transakcja była częścią otoczenia i odbywała się niejawnie w tle zamiast jawnie pod Twoją kontrolą, podejście TransactionScope może być dla Ciebie lepsze.

Podsumowując, w przypadku nowych interfejsów API Database.BeginTransaction() i Database.UseTransaction() powyżej podejście TransactionScope nie jest już konieczne dla większości użytkowników. Jeśli nadal używasz funkcji TransactionScope, pamiętaj o powyższych ograniczeniach. Zalecamy użycie podejścia opisanego w poprzednich sekcjach, jeśli jest to możliwe.