Práce s transakcemi

Poznámka

Pouze EF6 a novější – Funkce, rozhraní API atd. popsané na této stránce byly představeny v Entity Framework 6. Pokud používáte starší verzi, některé nebo všechny informace nemusí být platné.

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 ve výchozím nastavení dělá

Ve všech verzích Entity Framework se při každém spuštění SaveChanges() vloží, aktualizuje nebo odstraní v databázi, architektura tuto operaci zabalí 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í zabalí příkaz do transakce, pokud ještě neexistuje. Existují přetížení této metody, které umožňují přepsat toto chování, pokud chcete. Také při provádění uložených procedur obsažených v modelu prostřednictvím rozhraní API, jako je ObjectContext.ExecuteFunction(), provádí totéž (s tím rozdílem, že výchozí chování nelze v okamžiku přepsání).

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 rozhraním EF6 Entity Framework trval na otevření samotného připojení k databázi (v případě, že bylo předáno připojení, které už bylo otevřené). Vzhledem k tomu, že transakci lze spustit pouze u otevřeného připojení, znamená to, že jediný způsob, jak uživatel mohl zabalit několik operací do jedné transakce, byl buď použít TransactionScope, nebo použít ObjectContext.Připojeníion vlastnost a začít volat Open() a BeginTransaction() přímo na vrácený Entity Připojení ion 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 Připojení ion Management.

Od EF6 teď architektura poskytuje:

  1. Database.BeginTransaction() : Jednodušší metoda, jak uživatel spustit a dokončit transakce samotné v rámci existující DbContext – umožňuje několik operací kombinovat v rámci stejné transakce, a proto buď všechny potvrzené, nebo všechny vráceny zpět jako jedna. 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í úroveň IsolationLevel a jedno, které nepřijímá žádné argumenty a používá výchozí úroveň IsolationLevel od základního zprostředkovatele databáze. Obě přepsání vrací DbContextTransaction objekt, který poskytuje Commit() a Rollback(), které provádějí potvrzení a vrácení zpět 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 pomocí 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é základní 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ší v oboru a která zahrnuje operace se stejnou databází, ale mimo EF zcela. 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, musíte definovat a použít konstruktor ve vaší třídy kontextu, který dědí z jednoho z konstruktorů DbContext, které berou i) existující parametr připojení a ii) contextOwns Připojení ion boolean.

Poznámka

Příznak contextOwns Připojení ion 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 sql Připojení ion samotný, 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. Pří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();
                }
            }
        }
    }
}

Vymazání transakce

Můžete předat null Database.UseTransaction() vymazat Entity Framework znalosti aktuální transakce. Entity Framework nebude při tom zavádět ani vratit stávající transakci, proto používejte opatrně a pouze pokud si nejste 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 komunikují:

  • 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 doporučený způsob poskytování větších rozsah transakcí bylo použít TransactionScope objekt:

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 Připojení ion i Entity Framework by používaly okolí TransactionScope transakce, a proto byly 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í.
  • Pokud vydáte DDL a nepovolíte distribuované transakce prostřednictvím služby MSDTC, dojde k výjimkám.

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 transakci být okolí a zabývat se implicitně na pozadí spíše než explicitně pod kontrolou, pak TransactionScope přístup může vyhovovat vám lépe.

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.