메모
EF6 이후 버전만 - 이 페이지에서 설명하는 기능, API 등이 Entity Framework 6에 도입되었습니다. 이전 버전을 사용하는 경우 일부 또는 모든 정보가 적용되지 않습니다.
이 문서에서는 트랜잭션 작업을 쉽게 수행할 수 있도록 EF5 이후 추가한 향상된 기능을 포함하여 EF6에서 트랜잭션을 사용하는 방법을 설명합니다.
EF가 기본적으로 수행하는 일
모든 버전의 Entity Framework에서 SaveChanges() 를 실행하여 데이터베이스에 삽입, 업데이트 또는 삭제할 때마다 프레임워크는 트랜잭션에서 해당 작업을 래핑합니다. 이 트랜잭션은 작업을 실행할 수 있을 만큼 오래 지속된 다음 완료됩니다. 이러한 다른 작업을 실행하면 새 트랜잭션이 시작됩니다.
EF6 Database.ExecuteSqlCommand() 로 시작하면 기본적으로 트랜잭션에 명령이 아직 없는 경우 래핑됩니다. 원하는 경우 이 동작을 재정의할 수 있는 이 메서드의 오버로드가 있습니다. 또한 ObjectContext.ExecuteFunction()과 같은 API 를 통해 모델에 포함된 저장 프로시저의 EF6 실행에서도 동일한 작업을 수행합니다(현재 기본 동작을 재정의할 수 없음 제외).
두 경우 모두 트랜잭션의 격리 수준은 데이터베이스 공급자가 기본 설정으로 간주하는 격리 수준입니다. 예를 들어 SQL Server에서는 기본적으로 READ COMMITTED입니다.
Entity Framework는 트랜잭션에서 쿼리를 래핑하지 않습니다.
이 기본 기능은 많은 사용자에게 적합하며, 이 경우 EF6에서 다른 작업을 수행할 필요가 없습니다. 항상 그랬던 것처럼 코드를 작성하기만 하면 됩니다.
그러나 일부 사용자는 트랜잭션을 보다 세세하게 제어해야 합니다. 이 내용은 다음 섹션에서 설명합니다.
API 작동 방식
EF6 Entity Framework 이전에는 데이터베이스 연결 자체를 열어야 한다고 주장했습니다(이미 열려 있는 연결을 전달한 경우 예외가 발생했습니다). 열려 있는 연결에서만 트랜잭션을 시작할 수 있으므로 사용자가 여러 작업을 하나의 트랜잭션으로 래핑할 수 있는 유일한 방법은 TransactionScope를 사용하거나 ObjectContext.Connection 속성을 사용하고 반환된 EntityConnection 개체에서 직접 Open() 및 BeginTransaction() 호출을 시작하는 것이었습니다. 또한 기본 데이터베이스 연결에서 직접 트랜잭션을 시작한 경우 데이터베이스에 연결한 API 호출이 실패합니다.
메모
Entity Framework 6에서 닫힌 연결만 허용하는 제한 사항이 제거되었습니다. 자세한 내용은 연결 관리를 참조하세요.
EF6부터 프레임워크는 이제 다음을 제공합니다.
- Database.BeginTransaction() : 사용자가 기존 DbContext 내에서 트랜잭션을 직접 시작하고 완료하는 보다 쉬운 방법으로, 동일한 트랜잭션 내에서 여러 작업을 결합할 수 있으므로 커밋되거나 모두 하나의 트랜잭션으로 롤백됩니다. 또한 사용자가 트랜잭션에 대한 격리 수준을 보다 쉽게 지정할 수 있습니다.
- Database.UseTransaction() : DbContext가 Entity Framework 외부에서 시작된 트랜잭션을 사용할 수 있도록 합니다.
여러 작업을 동일한 컨텍스트 내의 하나의 트랜잭션으로 결합
Database.BeginTransaction() 에는 명시적 IsolationLevel 을 사용하는 재정의와 인수를 사용하지 않고 기본 데이터베이스 공급자의 기본 IsolationLevel을 사용하는 두 가지 재정의가 있습니다. 두 재정의 모두 기본 저장소 트랜잭션에서 커밋 및 롤백을 수행하는 Commit() 및 Rollback() 메서드를 제공하는 DbContextTransaction 개체를 반환합니다.
DbContextTransaction은 커밋되거나 롤백되면 삭제됩니다. 이 작업을 수행하는 한 가지 쉬운 방법은 using 블록이 완료될 때 Dispose()를 자동으로 호출하는 using(...) {...} 구문입니다.
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();
}
}
}
}
}
메모
트랜잭션을 시작하려면 기본 저장소 연결이 열려 있어야 합니다. 따라서 Database.BeginTransaction()을 호출하면 연결이 아직 열리지 않은 경우 연결이 열립니다. DbContextTransaction이 연결을 연 경우 Dispose()가 호출될 때 연결이 닫힙니다.
컨텍스트에 기존 트랜잭션 전달
경우에 따라 범위가 훨씬 더 넓고 동일한 데이터베이스에 대한 작업이 포함되지만 EF 외부에 있는 트랜잭션을 원하는 경우가 있습니다. 이렇게 하려면 연결을 열고 트랜잭션을 직접 시작한 다음 EF a) 이미 열려 있는 데이터베이스 연결을 사용하고 b) 해당 연결에서 기존 트랜잭션을 사용하도록 지시해야 합니다.
이를 수행하려면, 기존 연결 매개 변수와 contextOwnsConnection 부울을 사용하는 DbContext 생성자 중 하나에서 상속한 컨텍스트 클래스에 생성자를 정의하고 사용해야 합니다.
메모
이 시나리오에서 호출될 때 contextOwnsConnection 플래그를 false로 설정해야 합니다. 이것은 Entity Framework가 작업을 마쳤을 때 연결을 닫지 않도록 지시하는 것이므로 중요합니다 (예: 아래의 4번 줄 참조).
using (var conn = new SqlConnection("..."))
{
conn.Open();
using (var context = new BloggingContext(conn, contextOwnsConnection: false))
{
}
}
또한 직접 트랜잭션을 시작하고(기본 설정을 피하려면 IsolationLevel 포함) Entity Framework에 연결에서 이미 시작된 기존 트랜잭션이 있음을 알려야 합니다(아래 33줄 참조).
그런 다음 SqlConnection 자체 또는 DbContext에서 직접 데이터베이스 작업을 실행할 수 있습니다. 이러한 모든 작업은 하나의 트랜잭션 내에서 실행됩니다. 트랜잭션을 커밋하거나 롤백하고 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 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();
}
}
}
}
}
트랜잭션 정리
Database.UseTransaction()에 null을 전달하여 현재 트랜잭션에 대한 Entity Framework의 지식을 지울 수 있습니다. 이 작업을 수행할 때 Entity Framework는 기존 트랜잭션을 커밋하거나 롤백하지 않으므로 주의해서 사용해야 하며 이것이 원하는 작업이라고 확신하는 경우에만 사용합니다.
UseTransaction의 오류
다음과 같은 경우에 트랜잭션을 전달하는 경우 Database.UseTransaction()에서 예외가 표시됩니다.
- Entity Framework에 이미 기존 트랜잭션이 있음
- Entity Framework가 TransactionScope 내에서 이미 작동하고 있습니다.
- 전달된 트랜잭션의 연결 개체가 null입니다. 즉, 트랜잭션이 연결과 연결되지 않습니다. 일반적으로 트랜잭션이 이미 완료되었음을 나타내는 신호입니다.
- 전달된 트랜잭션의 연결 개체가 Entity Framework의 연결과 일치하지 않습니다.
다른 기능과 함께 트랜잭션 사용
이 섹션에서는 위의 트랜잭션이 상호 작용하는 방법을 자세히 설명합니다.
- 연결 복원력
- 비동기 메서드
- TransactionScope 트랜잭션
연결 복원력
새 연결 복원력 기능은 사용자가 시작한 트랜잭션에서 작동하지 않습니다. 자세한 내용은 실행 전략 다시 시도를 참조하세요.
비동기 프로그래밍
이전 섹션에서 설명한 접근 방식에는 비동기 쿼리 및 저장 메서드를 사용할 추가 옵션이나 설정이 필요하지 않습니다. 그러나 비동기 메서드 내에서 수행하는 작업에 따라 장기 실행 트랜잭션이 발생할 수 있으며 이로 인해 교착 상태가 발생하거나 차단이 발생하여 전체 애플리케이션의 성능이 저하될 수 있습니다.
TransactionScope 트랜잭션
EF6 이전에는 더 큰 범위 트랜잭션을 제공하는 권장 방법은 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 및 Entity Framework는 둘 다 앰비언트 TransactionScope 트랜잭션을 사용하므로 함께 커밋됩니다.
.NET 4.5.1 TransactionScope부터 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();
}
}
}
}
TransactionScope 접근 방식에는 여전히 몇 가지 제한 사항이 있습니다.
- 비동기 메서드를 사용하려면 .NET 4.5.1 이상이 필요합니다.
- 연결이 하나뿐이라고 확신하지 않는 한 클라우드 시나리오에서 사용할 수 없습니다(클라우드 시나리오는 분산 트랜잭션을 지원하지 않음).
- 이전 섹션의 Database.UseTransaction() 접근 방식과 결합할 수 없습니다.
- DDL을 발급하고 MSDTC 서비스를 통해 분산 트랜잭션을 사용하도록 설정하지 않은 경우, 예외가 발생합니다.
TransactionScope 접근 방식의 이점:
- 지정된 데이터베이스에 둘 이상의 연결을 만들거나 동일한 트랜잭션 내의 다른 데이터베이스에 대한 연결을 사용하여 한 데이터베이스에 대한 연결을 결합하는 경우 로컬 트랜잭션을 분산 트랜잭션으로 자동으로 업그레이드합니다(참고: 분산 트랜잭션이 작동하도록 MSDTC 서비스가 구성되어 있어야 함).
- 코딩의 용이성. 트랜잭션이 앰비언트되고 명시적으로 제어하는 대신 백그라운드에서 암시적으로 처리되는 것을 선호하는 경우 TransactionScope 접근 방식이 더 적합할 수 있습니다.
요약하자면, 위의 새 Database.BeginTransaction() 및 Database.UseTransaction() API를 사용하면 대부분의 사용자에게 TransactionScope 접근 방식이 더 이상 필요하지 않습니다. TransactionScope를 계속 사용하는 경우 위의 제한 사항에 유의하세요. 가능한 경우 이전 섹션에 설명된 방법을 대신 사용하는 것이 좋습니다.
.NET