Bekerja dengan Transaksi

Nota

EF6 Onwards Only - Fitur, API, dll. yang dibahas di halaman ini diperkenalkan dalam Entity Framework 6. Jika Anda menggunakan versi yang lebih lama, beberapa atau semua informasi tidak berlaku.

Dokumen ini akan menjelaskan penggunaan transaksi di EF6 termasuk penyempurnaan yang telah kami tambahkan sejak EF5 untuk memudahkan bekerja dengan transaksi.

Apa yang dilakukan EF secara default

Di semua versi Entity Framework, setiap kali Anda menjalankan SaveChanges() untuk menyisipkan, memperbarui, atau menghapus pada database, kerangka kerja akan membungkus operasi tersebut dalam transaksi. Transaksi ini hanya berlangsung cukup lama untuk menjalankan operasi dan kemudian selesai. Ketika Anda menjalankan operasi lain seperti itu, transaksi baru dimulai.

Dimulai dari EF6, Database.ExecuteSqlCommand() secara default akan melakukan operasi perintah dengan membungkusnya dalam transaksi jika belum ada. Ada kelebihan beban metode ini yang memungkinkan Anda untuk mengambil alih perilaku ini jika Anda mau. Juga, dalam eksekusi prosedur tersimpan di EF6 yang disertakan dalam model melalui API seperti ObjectContext.ExecuteFunction() melakukan proses yang sama (kecuali bahwa saat ini perilaku default tidak dapat diubah).

Dalam kedua kasus, tingkat isolasi transaksi adalah tingkat isolasi apa pun yang penyedia database pertimbangkan pengaturan defaultnya. Secara default, misalnya, di SQL Server, ini adalah READ COMMITTED.

Entity Framework tidak membungkus kueri dalam transaksi.

Fungsionalitas default ini cocok untuk banyak pengguna dan jika demikian tidak perlu melakukan sesuatu yang berbeda di EF6; hanya menulis kode seperti yang selalu Anda lakukan.

Namun beberapa pengguna memerlukan kontrol yang lebih besar atas transaksi mereka - ini tercakup dalam bagian berikut.

Cara kerja API

Sebelum EF6 Entity Framework bersikeras membuka koneksi database itu sendiri (menghasilkan pengecualian jika diberi koneksi yang sudah terbuka). Karena transaksi hanya dapat dimulai pada koneksi terbuka, ini berarti bahwa satu-satunya cara pengguna dapat membungkus beberapa operasi ke dalam satu transaksi adalah dengan menggunakan TransactionScope atau menggunakan properti ObjectContext.Connection dan mulai memanggil Open() dan BeginTransaction() langsung pada objek EntityConnection yang dikembalikan. Selain itu, panggilan API yang menghubungi database akan gagal jika Anda telah memulai transaksi pada koneksi database yang mendasar sendiri.

Nota

Batasan hanya menerima koneksi tertutup dihapus dalam Kerangka Kerja Entitas 6. Untuk detailnya, lihat Manajemen Koneksi.

Dimulai dengan EF6, kerangka kerja sekarang menyediakan:

  1. Database.BeginTransaction() : Merupakan metode yang lebih mudah bagi pengguna untuk memulai dan menyelesaikan transaksi sendiri dalam DbContext yang ada – memungkinkan beberapa operasi digabungkan dalam transaksi yang sama dan oleh karena itu, semuanya dapat dikonfirmasi atau dibatalkan sebagai satu kesatuan. Ini juga memungkinkan pengguna untuk lebih mudah menentukan tingkat isolasi untuk transaksi.
  2. Database.UseTransaction() : yang memungkinkan DbContext menggunakan transaksi yang dimulai di luar Kerangka Kerja Entitas.

Menggabungkan beberapa operasi ke dalam satu transaksi dalam konteks yang sama

Database.BeginTransaction() memiliki dua penggantian – satu yang menerima IsolationLevel eksplisit dan satu lagi yang tidak menerima argumen dan menggunakan IsolationLevel default dari penyedia database yang mendasar. Kedua penimpaan mengembalikan objek DbContextTransaction yang menyediakan metode Commit() dan Rollback() yang melaksanakan komit dan rollback pada transaksi penyimpanan yang berada di bawahnya.

DbContextTransaction dimaksudkan untuk dihapus setelah dikonfirmasi atau digulung balik. Salah satu cara mudah untuk mencapai hal ini adalah sintaksis using(...) {...} yang akan secara otomatis memanggil Dispose() ketika blok penggunaan selesai:

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

Memulai transaksi mengharuskan koneksi penyimpanan dasar terbuka. Jadi memanggil Database.BeginTransaction() akan membuka koneksi jika belum dibuka. Jika DbContextTransaction membuka koneksi, maka DbContextTransaction akan menutupnya ketika Dispose() dipanggil.

Meneruskan transaksi yang ada ke konteks

Terkadang Anda menginginkan transaksi yang bahkan lebih luas dalam cakupan dan yang mencakup operasi pada database yang sama tetapi di luar EF sepenuhnya. Untuk mencapai hal ini, Anda harus membuka koneksi dan memulai transaksi sendiri lalu memberi tahu EF a) untuk menggunakan koneksi database yang sudah dibuka, dan b) untuk menggunakan transaksi yang ada pada koneksi tersebut.

Untuk melakukan ini, Anda harus menentukan dan menggunakan konstruktor pada kelas konteks Anda yang mewarisi dari salah satu konstruktor DbContext yang mengambil i) parameter koneksi yang ada dan ii) boolean contextOwnsConnection.

Nota

Parameter contextOwnsConnection harus disetel ke 'false' ketika dipanggil dalam skenario ini. Ini penting karena menginformasikan Entity Framework bahwa ia tidak boleh menutup koneksi saat sudah selesai menggunakannya (misalnya, lihat baris 4 di bawah):

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

Selain itu, Anda harus memulai transaksi sendiri (termasuk IsolationLevel jika Anda ingin menghindari pengaturan default) dan memberi tahu Kerangka Kerja Entitas bahwa ada transaksi yang sudah dimulai pada koneksi (lihat baris 33 di bawah).

Kemudian Anda bebas untuk menjalankan operasi database baik langsung di SqlConnection itu sendiri, atau di DbContext. Semua operasi tersebut dijalankan dalam satu transaksi. Anda bertanggung jawab untuk melakukan atau mengembalikan transaksi dan untuk memanggil Dispose(), serta untuk menutup dan membuang koneksi basis data. Contohnya:

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

Membersihkan transaksi

Anda dapat meneruskan null ke Database.UseTransaction() untuk menghapus pengetahuan Entity Framework tentang transaksi saat ini. Kerangka Kerja Entitas tidak akan menerapkan atau memutar kembali transaksi yang ada saat Anda melakukan ini, jadi gunakan dengan hati-hati dan hanya jika Anda yakin ini adalah apa yang ingin Anda lakukan.

Kesalahan dalam UseTransaction

Anda akan melihat pengecualian dari Database.UseTransaction() jika Anda meneruskan transaksi ketika:

  • Kerangka Kerja Entitas sudah memiliki transaksi yang ada
  • Kerangka Kerja Entitas sudah beroperasi dalam TransactionScope
  • Objek koneksi dalam transaksi yang telah diteruskan adalah null. Artinya, transaksi tidak terkait dengan koneksi - biasanya ini adalah tanda bahwa transaksi telah selesai
  • Objek koneksi dalam transaksi yang diteruskan tidak cocok dengan koneksi Kerangka Kerja Entitas.

Menggunakan transaksi dengan fitur lain

Bagian ini merinci bagaimana transaksi di atas berinteraksi dengan:

  • Ketahanan koneksi
  • Metode asinkron
  • TransactionScope Transaksi

Ketahanan Koneksi

Fitur Ketahanan Koneksi baru tidak berfungsi dengan transaksi yang dimulai pengguna. Untuk detailnya, lihat Mencoba Kembali Strategi Eksekusi.

Pemrograman Asinkron

Pendekatan yang diuraikan di bagian sebelumnya tidak memerlukan opsi atau pengaturan lebih lanjut untuk bekerja dengan kueri asinkron dan metode penyimpanan. Tetapi ketahuilah bahwa, tergantung pada apa yang Anda lakukan dalam metode asinkron, ini dapat mengakibatkan transaksi yang berjalan lama - yang pada gilirannya dapat menyebabkan kebuntuan atau pemblokiran yang buruk untuk performa aplikasi keseluruhan.

Transaksi TransactionScope

Sebelum EF6, cara yang direkomendasikan untuk menyediakan transaksi cakupan yang lebih besar adalah dengan menggunakan objek 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 dan Entity Framework akan menggunakan transaksi TransactionScope yang tersedia di lingkungan dan karenanya diselesaikan bersama-sama.

Dimulai dengan .NET 4.5.1 TransactionScope telah diperbarui untuk juga bekerja dengan metode asinkron melalui penggunaan enumerasi 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();
            }
        }
    }
}

Masih ada beberapa batasan untuk pendekatan TransactionScope:

  • Memerlukan .NET 4.5.1 atau lebih besar untuk bekerja dengan metode asinkron.
  • Ini tidak dapat digunakan dalam skenario cloud kecuali Anda yakin Anda memiliki satu dan hanya satu koneksi (skenario cloud tidak mendukung transaksi terdistribusi).
  • Ini tidak dapat dikombinasikan dengan pendekatan Database.UseTransaction() dari bagian sebelumnya.
  • Ini akan melemparkan pengecualian jika Anda mengeluarkan DDL dan belum mengaktifkan transaksi terdistribusi melalui Layanan MSDTC.

Keuntungan dari pendekatan TransactionScope:

  • Ini akan secara otomatis meningkatkan transaksi lokal ke transaksi terdistribusi jika Anda membuat lebih dari satu koneksi ke database tertentu atau menggabungkan koneksi ke satu database dengan koneksi ke database yang berbeda dalam transaksi yang sama (catatan: Anda harus memiliki layanan MSDTC yang dikonfigurasi untuk memungkinkan transaksi terdistribusi agar ini berfungsi).
  • Kemudahan pengkodian. Jika Anda lebih suka transaksi menjadi terintegrasi dan ditangani secara implisit di latar belakang daripada di bawah kontrol langsung Anda, pendekatan TransactionScope mungkin lebih cocok untuk Anda.

Singkatnya, dengan API Database.BeginTransaction() dan Database.UseTransaction() baru di atas, pendekatan TransactionScope tidak lagi diperlukan untuk sebagian besar pengguna. Jika Anda terus menggunakan TransactionScope, maka ketahui batasan di atas. Sebaiknya gunakan pendekatan yang diuraikan di bagian sebelumnya sebagai gantinya jika memungkinkan.