Práce s transakcemi

Poznámka:

Pouze EF6 – funkce, rozhraní API atd. probírané na této stránce byly představeny v Entity Frameworku 6. Pokud používáte starší verzi, některé nebo všechny informace se nevztahují.

Tento dokument popisuje použití transakcí v EF6 včetně vylepšení, která jsme přidali od EF5, aby bylo možné snadno pracovat s transakcemi.

Co EF dělá ve výchozím nastavení

Ve všech verzích Entity Framework, při každém zavolání SaveChanges() pro vložení, aktualizaci nebo odstranění objektů v databázi, je tato operace v rámci zabalena do transakce. Tato transakce trvá dostatečně dlouho, aby se operace spustila a pak se dokončila. Když spustíte jinou takovou operaci, spustí se nová transakce.

Počínaje verzí EF6, Database.ExecuteSqlCommand() ve výchozím nastavení příkaz zabalí do transakce, pokud žádná transakce není již přítomna. Existují přetížené verze této metody, které vám umožňují přepsat toto chování, pokud si přejete. Také při provádění uložených procedur, obsažených v modelu prostřednictvím rozhraní API, jako je ObjectContext.ExecuteFunction(), se provádějí stejným způsobem (s tím rozdílem, že výchozí chování nelze v tuto chvíli změnit).

V obou případech je úroveň izolace transakce jakákoli úroveň izolace, která poskytovatel databáze považuje za výchozí nastavení. Ve výchozím nastavení se například na SQL Serveru jedná o potvrzené čtení.

Entity Framework nezabaluje dotazy do transakce.

Tato výchozí funkce je vhodná pro mnoho uživatelů, a pokud není potřeba dělat nic jiného v EF6; stačí napsat kód tak, jak jste to vždycky udělali.

Někteří uživatelé ale vyžadují větší kontrolu nad svými transakcemi – to je popsáno v následujících částech.

Jak rozhraní API fungují

Před EF6 Entity Framework trval na otevření samotného připojení k databázi (vyhodilo výjimku, pokud bylo předáno již otevřené připojení). Vzhledem k tomu, že transakci lze spustit pouze u otevřeného připojení, to znamená, že jediný způsob, jak uživatel mohl zabalit několik operací do jedné transakce, byl buď použít TransactionScope , nebo použít ObjectContext.Connection vlastnost a začít volat Open() a BeginTransaction() přímo na vrácený EntityConnection objektu. Kromě toho volání rozhraní API, která kontaktovala databázi, by selhala, kdybyste spustili transakci u základního databázového připojení sami.

Poznámka:

Omezení pouze přijímání uzavřených připojení bylo odebráno v entity Framework 6. Podrobnosti najdete v tématu Správa připojení.

Od EF6 teď architektura poskytuje:

  1. Database.BeginTransaction() : Snazší metoda pro uživatele, jak zahájit a dokončit transakce v rámci existujícího DbContextu – umožňuje kombinovat několik operací v rámci stejné transakce, a tudíž buď všechny potvrdit, nebo všechny vrátit zpět jako jeden celek. Umožňuje také uživateli lépe určit úroveň izolace transakce.
  2. Database.UseTransaction() : což umožňuje DbContext použít transakci, která byla spuštěna mimo Entity Framework.

Kombinování několika operací do jedné transakce ve stejném kontextu

Database.BeginTransaction() má dvě přepsání – jedno přebírá explicitní IsolationLevel a jedno, které nepřijímá žádné argumenty a používá výchozí IsolationLevel od základního zprostředkovatele databáze. Obě varianty vrací objekt DbContextTransaction, který poskytuje metody Commit() a Rollback(), jež provádějí potvrzení a zrušení v podkladové transakci úložiště.

DbContextTransaction je určen k vyřazení po potvrzení nebo vrácení zpět. Jedním ze snadných způsobů, jak toho dosáhnout, je syntaxe using(...) {...}, která po dokončení bloku 'using' automaticky zavolá Dispose():

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

Poznámka:

Zahájení transakce vyžaduje, aby bylo otevřené podkladové připojení k úložišti. Volání Database.BeginTransaction() tedy otevře připojení, pokud ještě není otevřeno. Pokud DbContextTransaction otevřelo připojení, zavře ho při zavolání Dispose().

Předání existující transakce do kontextu

Někdy byste chtěli transakci, která je ještě širšího rozsahu a která zahrnuje operace se stejnou databází, ale zcela mimo EF. Chcete-li toho dosáhnout, musíte otevřít připojení a spustit transakci sami a pak řekněte EF a) použít již otevřené databázové připojení, a b) k použití existující transakce v tomto připojení.

Chcete-li to provést, je nutné definovat a použít konstruktor ve vaší kontextové třídě, který dědí z jednoho z konstruktorů DbContext, které berou i) existující parametr připojení a ii) contextOwnsConnection boolean.

Poznámka:

Příznak contextOwnsConnection musí být při zavolání v tomto scénáři nastaven na false. To je důležité, protože informuje Entity Framework, že by neměl při jeho dokončení zavřít připojení (například viz řádek 4 níže):

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

Kromě toho musíte transakci spustit sami (včetně IsolationLevel, pokud se chcete vyhnout výchozímu nastavení) a dejte Entity Framework vědět, že existuje existující transakce již spuštěna v připojení (viz řádek 33 níže).

Pak můžete provádět databázové operace buď přímo na samotném sqlConnectionu, nebo v DbContext. Všechny tyto operace se provádějí v rámci jedné transakce. Zodpovídáte za potvrzení nebo vrácení transakce a za volání Dispose() na ni, jakož i za uzavření a vyřazení připojení k databázi. Například:

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

Řešení transakce

Můžete předat hodnotu null do Database.UseTransaction(), abyste vymazali znalosti Entity Framework o aktuální transakci. Entity Framework při tomto neprovede ani nevrátí stávající transakci, proto používejte opatrně a pouze pokud jste si jistí, že to chcete udělat.

Chyby v příkazu UseTransaction

Pokud předáte transakci, zobrazí se výjimka z Database.UseTransaction(), když:

  • Entity Framework již má existující transakci.
  • Entity Framework už pracuje v rámci TransactionScope.
  • Objekt připojení v předané transakci je null. To znamená, že transakce není přidružena k připojení – obvykle se jedná o znaménko, že transakce již byla dokončena.
  • Objekt připojení v předané transakci neodpovídá připojení Entity Framework.

Použití transakcí s jinými funkcemi

Tato část podrobně popisuje, jak výše uvedené transakce interagují s:

  • Odolnost připojení
  • Asynchronní metody
  • Transakce TransactionScope

Odolnost připojení

Nová funkce odolnosti připojení nefunguje s transakcemi iniciovanými uživatelem. Podrobnosti najdete v tématu Opakování strategií provádění.

Asynchronní programování

Přístup popsaný v předchozích částech nepotřebuje žádné další možnosti ani nastavení pro práci s asynchronním dotazem a metodami ukládání. Mějte ale na paměti, že v závislosti na tom, co děláte v rámci asynchronních metod, může to vést k dlouhotrvajícím transakcím – což může zase způsobit zablokování nebo blokování, což je špatné pro výkon celkové aplikace.

Transakce TransactionScope

Před EF6 byl doporučený způsob poskytování větších rozsahů transakcí použít objekt 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();
            }
        }
    }
}

SqlConnection i Entity Framework by používaly okolí TransactionScope transakce, a proto být potvrzeny společně.

Počínaje rozhraním .NET 4.5.1 TransactionScope jsme aktualizovali také práci s asynchronními metodami prostřednictvím použití výčtu 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();
            }
        }
    }
}

Přístup TransactionScope má stále určitá omezení:

  • Vyžaduje, aby rozhraní .NET 4.5.1 nebo novější fungovalo s asynchronními metodami.
  • Nejde ho použít ve scénářích cloudu, pokud si nejste jistí, že máte jedno a jenom jedno připojení (cloudové scénáře nepodporují distribuované transakce).
  • Nelze ji kombinovat s přístupem Database.UseTransaction() předchozích částí.
  • Vyvolá výjimky, pokud vydáte DDL a nepovolíte distribuované transakce prostřednictvím služby MSDTC.

Výhody přístupu TransactionScope:

  • Automaticky provede upgrade místní transakce na distribuovanou transakci, pokud provedete více než jedno připojení k dané databázi nebo zkombinujete připojení k jedné databázi s připojením k jiné databázi v rámci stejné transakce (poznámka: musíte mít službu MSDTC nakonfigurovanou tak, aby tato operace fungovala).
  • Snadné kódování. Pokud dáváte přednost, aby transakce byla automatizovaná a probíhala automaticky na pozadí než explicitně pod vaší kontrolou, pak vám může TransactionScope přístup lépe vyhovovat.

Stručně řečeno, s novým rozhraním API Database.BeginTransaction() a Database.UseTransaction() výše není přístup TransactionScope pro většinu uživatelů nezbytný. Pokud budete nadále používat TransactionScope, mějte na paměti výše uvedená omezení. Pokud je to možné, doporučujeme místo toho použít přístup popsaný v předchozích částech.