트랜잭션 사용

트랜잭션을 사용하면 여러 데이터베이스 작업을 원자성 방식으로 처리할 수 있습니다. 트랜잭션이 커밋되면 모든 작업이 성공적으로 데이터베이스에 적용됩니다. 트랜잭션이 롤백되면 데이터베이스에 아무 작업도 적용되지 않습니다.

GitHub에서 이 문서의 샘플을 볼 수 있습니다.

기본 트랜잭션 동작

기본적으로 데이터베이스 공급자가 트랜잭션을 지원하는 경우 SaveChanges에 대한 단일 호출의 모든 변경 내용이 트랜잭션에 적용됩니다. 변경이 실패하면 트랜잭션이 롤백되고 변경 내용이 데이터베이스에 적용되지 않습니다. 즉, SaveChanges이 완전히 성공하도록 보장되거나 오류가 발생하는 경우 데이터베이스가 수정되지 않은 상태로 유지됩니다.

대부분의 애플리케이션에서는 이 기본 동작이면 충분합니다. 애플리케이션 요구 사항에서 필요하다고 생각되는 경우에만 트랜잭션을 수동으로 제어해야 합니다.

트랜잭션 제어

DbContext.Database API를 사용하여 트랜잭션을 시작하고 커밋하고 롤백할 수 있습니다. 다음 예제에서는 단일 트랜잭션에서 실행되는 두 개의 SaveChanges 작업과 LINQ 쿼리를 보여 줍니다.

using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();

try
{
    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    context.SaveChanges();

    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
    context.SaveChanges();

    var blogs = context.Blogs
        .OrderBy(b => b.Url)
        .ToList();

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
catch (Exception)
{
    // TODO: Handle failure
}

모든 관계형 데이터베이스 공급자는 트랜잭션을 지원하지만 트랜잭션 API가 호출될 때 다른 공급자 형식이 throw 또는 no-op될 수 있습니다.

참고 항목

이러한 방식으로 트랜잭션을 수동으로 제어하는 것은 암시적으로 호출된 다시 시도 실행 전략과 호환되지 않습니다. 자세한 내용은 연결 복원력을 참조하세요.

저장점

SaveChanges가 호출되고 트랜잭션이 컨텍스트에서 이미 진행 중이면 EF는 데이터를 저장하기 전에 자동으로 ‘저장점’을 만듭니다. 저장점은 나중에 오류가 발생한 경우나 다른 이유로 롤백될 수 있는 데이터베이스 트랜잭션 내의 지점입니다. SaveChanges에서 오류가 발생하는 경우 트랜잭션을 자동으로 저장점으로 롤백하여 트랜잭션을 시작되지 않은 것처럼 동일한 상태로 유지합니다. 이렇게 하면 특히 낙관적 동시성 문제가 발생할 때 문제를 수정하고 다시 저장할 수 있습니다.

Warning

저장점은 SQL Server의 MARS(Multiple Active Result Set)와 호환되지 않습니다. MARS가 적극적으로 사용되지 않더라도 연결에서 MARS를 사용하도록 설정하면 EF에서 저장점이 생성되지 않습니다. SaveChange 중에 오류가 발생하면 트랜잭션이 알 수 없는 상태로 남아 있을 수 있습니다.

트랜잭션과 마찬가지로 저장점을 수동으로 관리할 수도 있습니다. 다음 예제에서는 트랜잭션 내에 저장점을 만들고 오류 발생 시 해당 저장점으로 롤백합니다.

using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();

try
{
    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/dotnet/" });
    context.SaveChanges();

    transaction.CreateSavepoint("BeforeMoreBlogs");

    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/visualstudio/" });
    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/aspnet/" });
    context.SaveChanges();

    transaction.Commit();
}
catch (Exception)
{
    // If a failure occurred, we rollback to the savepoint and can continue the transaction
    transaction.RollbackToSavepoint("BeforeMoreBlogs");

    // TODO: Handle failure, possibly retry inserting blogs
}

컨텍스트 간 트랜잭션

여러 컨텍스트 인스턴스 간에 트랜잭션을 공유할 수도 있습니다. 이 기능은 관계형 데이터베이스와 관련된 DbTransactionDbConnection을 사용해야 하므로 관계형 데이터베이스 공급자를 사용하는 경우에만 사용할 수 있습니다.

트랜잭션을 공유하려면 컨텍스트가 DbConnectionDbTransaction을 모두 공유해야 합니다.

연결을 외부에서 제공하도록 허용

DbConnection을 공유하려면 이를 구성할 때 컨텍스트로 연결을 전달하는 기능이 필요합니다.

DbConnection을 외부에서 제공하도록 하는 가장 쉬운 방법은 DbContext.OnConfiguring 메서드를 사용하여 컨텍스트를 구성하고 DbContextOptions를 외부에서 만들어 컨텍스트 생성자에 전달하는 것을 중지하는 것입니다.

DbContextOptionsBuilderDbContext.OnConfiguring에서 컨텍스트를 구성하는 데 사용되는 API이며, 이제 외부에서 이 API를 사용하여 DbContextOptions를 만듭니다.

public class BloggingContext : DbContext
{
    public BloggingContext(DbContextOptions<BloggingContext> options)
        : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
}

또는 DbContext.OnConfiguring을 계속 사용하지만 DbContext.OnConfiguring에서 저장된 후 사용되는 DbConnection을 허용합니다.

public class BloggingContext : DbContext
{
    private DbConnection _connection;

    public BloggingContext(DbConnection connection)
    {
      _connection = connection;
    }

    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(_connection);
    }
}

연결 및 트랜잭션 공유

이제 동일한 연결을 공유하는 여러 컨텍스트 인스턴스를 작성할 수 있습니다. 그런 다음, DbContext.Database.UseTransaction(DbTransaction) API를 사용하여 두 컨텍스트를 동일한 트랜잭션에 등록합니다.

using var connection = new SqlConnection(connectionString);
var options = new DbContextOptionsBuilder<BloggingContext>()
    .UseSqlServer(connection)
    .Options;

using var context1 = new BloggingContext(options);
using var transaction = context1.Database.BeginTransaction();
try
{
    context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    context1.SaveChanges();

    using (var context2 = new BloggingContext(options))
    {
        context2.Database.UseTransaction(transaction.GetDbTransaction());

        var blogs = context2.Blogs
            .OrderBy(b => b.Url)
            .ToList();

        context2.Blogs.Add(new Blog { Url = "http://dot.net" });
        context2.SaveChanges();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
catch (Exception)
{
    // TODO: Handle failure
}

외부 DbTransactions 사용(관계형 데이터베이스만 해당)

여러 데이터 액세스 기술을 사용하여 관계형 데이터베이스에 액세스하는 경우 이러한 서로 다른 기술에서 수행하는 작업 간에 트랜잭션을 공유할 수 있습니다.

다음 예제에서는 동일한 트랜잭션에서 ADO.NET SqlClient 작업 및 Entity Framework Core 작업을 수행하는 방법을 보여줍니다.

using var connection = new SqlConnection(connectionString);
connection.Open();

using var transaction = connection.BeginTransaction();
try
{
    // Run raw ADO.NET command in the transaction
    var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.CommandText = "DELETE FROM dbo.Blogs";
    command.ExecuteNonQuery();

    // Run an EF Core command in the transaction
    var options = new DbContextOptionsBuilder<BloggingContext>()
        .UseSqlServer(connection)
        .Options;

    using (var context = new BloggingContext(options))
    {
        context.Database.UseTransaction(transaction);
        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context.SaveChanges();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
catch (Exception)
{
    // TODO: Handle failure
}

System.Transactions 사용

더 큰 범위를 조정해야 하는 경우 앰비언트 트랜잭션을 사용할 수 있습니다.

using (var scope = new TransactionScope(
           TransactionScopeOption.Required,
           new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
    using var connection = new SqlConnection(connectionString);
    connection.Open();

    try
    {
        // Run raw ADO.NET command in the transaction
        var command = connection.CreateCommand();
        command.CommandText = "DELETE FROM dbo.Blogs";
        command.ExecuteNonQuery();

        // Run an EF Core command in the transaction
        var options = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlServer(connection)
            .Options;

        using (var context = new BloggingContext(options))
        {
            context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
            context.SaveChanges();
        }

        // Commit transaction if all commands succeed, transaction will auto-rollback
        // when disposed if either commands fails
        scope.Complete();
    }
    catch (Exception)
    {
        // TODO: Handle failure
    }
}

또한 명시적 트랜잭션에 등록할 수 있습니다.

using (var transaction = new CommittableTransaction(
           new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
    var connection = new SqlConnection(connectionString);

    try
    {
        var options = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlServer(connection)
            .Options;

        using (var context = new BloggingContext(options))
        {
            context.Database.OpenConnection();
            context.Database.EnlistTransaction(transaction);

            // Run raw ADO.NET command in the transaction
            var command = connection.CreateCommand();
            command.CommandText = "DELETE FROM dbo.Blogs";
            command.ExecuteNonQuery();

            // Run an EF Core command in the transaction
            context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
            context.SaveChanges();
            context.Database.CloseConnection();
        }

        // Commit transaction if all commands succeed, transaction will auto-rollback
        // when disposed if either commands fails
        transaction.Commit();
    }
    catch (Exception)
    {
        // TODO: Handle failure
    }
}

참고 항목

비동기 API를 사용하는 경우 TransactionScope 생성자에서 TransactionScopeAsyncFlowOption.Enabled를 지정하여 비동기 호출 간에 앰비언트 트랜잭션이 흐르도록 해야 합니다.

TransactionScope 및 앰비언트 트랜잭션에 대한 자세한 내용은 설명서를 참조하세요.

System.Transactions의 제한 사항

  1. EF Core는 데이터베이스 공급자를 사용하여 System.Transactions에 대한 지원을 구현합니다. 공급자가 System.Transactions에 대한 지원을 구현하지 않는 경우 이러한 API에 대한 호출을 완전히 무시해도 됩니다. SqlClient는 이 기능을 지원합니다.

    Important

    트랜잭션을 관리하는 데 사용하기 전에 API가 공급자에서 올바르게 동작하는지 테스트하는 것이 좋습니다. 그렇지 않으면 데이터베이스 공급자의 유지 관리자에게 문의하는 것이 좋습니다.

  2. System.Transactions의 분산 트랜잭션 지원은 Windows용 .NET 7.0에만 추가되었습니다. 이전 .NET 버전 또는 비 Windows 플랫폼에서 분산 트랜잭션을 사용하려는 모든 시도는 실패합니다.

  3. TransactionScope는 비동기 커밋/롤백을 지원하지 않습니다. 즉, 삭제하면 작업이 완료될 때까지 실행 중인 스레드가 동기적으로 차단됩니다.