Praca z transakcjami

Uwaga

Tylko rozwiązanie EF6 i nowsze wersje — Funkcje, interfejsy API itp. omówione na tej stronie zostały wprowadzone w rozwiązaniu Entity Framework 6. Jeśli korzystasz ze starszej wersji, niektóre lub wszystkie podane 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 wystarczająco długo, aby wykonać operację, a następnie kończy. Po wykonaniu innej takiej operacji zostanie uruchomiona nowa transakcja.

Począwszy od ef6 Database.ExecuteSqlCommand() domyślnie zawija polecenie w transakcji, jeśli jeszcze nie było. Istnieją przeciążenia tej metody, które umożliwiają zastąpienie tego zachowania, jeśli chcesz. 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 obu przypadkach poziom izolacji transakcji jest niezależnie od poziomu izolacji, który dostawca bazy danych uznaje za domyślne ustawienie. 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

Przed programem EF6 Entity Framework nalegał na otwarcie samego połączenia z bazą danych (zgłosił wyjątek, jeśli został przekazany połączenie, które było już otwarte). Ponieważ transakcja może zostać uruchomiona 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 elementu TransactionScope lub użycie obiektu ObjectContext.Połączeniewłaściwość ion i rozpocząć wywoływanie metody Open() i BeginTransaction() bezpośrednio w zwróconym obiekcie Entity Połączenie ion. 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

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łączenie ion.

Począwszy od platformy EF6, platforma zapewnia teraz:

  1. Database.BeginTransaction() : łatwiejsza metoda uruchamiania i wykonywania transakcji przez użytkownika w ramach istniejącej bazy danychKontekst — umożliwia łączenie kilku operacji w ramach tej samej transakcji, a tym samym wszystkie zatwierdzone lub wszystkie wycofane 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

Rozpoczęcie transakcji wymaga otwarcia podstawowego połączenia magazynu. 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 chcesz, aby transakcja, która jest jeszcze szersza w zakresie i która obejmuje operacje na tej samej bazie danych, ale poza ef całkowicie. Aby to osiągnąć, należy samodzielnie otworzyć połączenie i uruchomić transakcję, a następnie poinformować ef a), aby użyć już otwartego połączenia bazy danych i b) do korzystania 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 i ii) contextOwns Połączenie ion wartość logiczna.

Uwaga

Flaga contextOwns Połączenie ion musi być ustawiona na wartość false po wywołaniu w tym scenariuszu. Jest to ważne, ponieważ informuje program Entity Framework, że nie powinien zamykać połączenia po zakończeniu z nim (na przykład zobacz wiersz 4 poniżej):

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

Ponadto należy samodzielnie uruchomić transakcję (w tym izolacjęLevel, jeśli chcesz uniknąć ustawienia domyślnego) i poinformować platformę Entity Framework, że istnieje już transakcja uruchomiona na połączeniu (zobacz wiersz 33 poniżej).

Następnie możesz wykonywać operacje bazy danych bezpośrednio na samym serwerze Sql Połączenie ion 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 z parametru Database.UseTransaction() w następujących przypadkach:

  • Program Entity Framework ma już istniejącą transakcję
  • Program Entity Framework działa już w ramach elementu 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:

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

Elastyczność połączenia

Nowa funkcja odporności Połączenie ion 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 spowodować zakleszczenia lub zablokowanie, co jest złe dla wydajności ogólnej aplikacji.

TransactionScope Transactions

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 sql Połączenie ion, jak i Entity Framework używają otoczenia transakcji TransactionScope, a tym samym zostaną zatwierdzone razem.

Począwszy od platformy .NET 4.5.1 TransactionScope został zaktualizowany w celu pracy z metodami asynchronicznymi przy użyciu 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 w przypadku wystawienia dowolnego pliku DDL i nie włączono 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ł otoczenia i zajmowała się niejawnie w tle, a nie jawnie pod kontrolą, podejście TransactionScope może być 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.