Skip to content

Commit

Permalink
Add savepoint support
Browse files Browse the repository at this point in the history
* Add APIs for supporting transaction savepoints.
* Support is implemented at the EF level only, no System.Data support
  yet (this should come soon).
* Use savepoints in the update pipeline when a user-managed transaction
  is used, to roll back to before SaveChanges in case of exception.

Part of #20176
  • Loading branch information
roji committed Apr 24, 2020
1 parent 55d578f commit 54e479e
Show file tree
Hide file tree
Showing 17 changed files with 762 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public virtual Task<IDbContextTransaction> BeginTransactionAsync(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual Task CommitTransactionAsync(CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
=> throw new NotSupportedException();

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
36 changes: 36 additions & 0 deletions src/EFCore.InMemory/Storage/Internal/InMemoryTransactionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,42 @@ public virtual Task RollbackTransactionAsync(CancellationToken cancellationToken
return Task.CompletedTask;
}

/// <inheritdoc />
public virtual void CreateSavepoint(string savepointName)
=> _logger.TransactionIgnoredWarning();

/// <inheritdoc />
public virtual Task CreateSavepointAsync(string savepointName, CancellationToken cancellationToken = default)
{
_logger.TransactionIgnoredWarning();
return Task.CompletedTask;
}

/// <inheritdoc />
public virtual void RollbackSavepoint(string savepointName)
=> _logger.TransactionIgnoredWarning();

/// <inheritdoc />
public virtual Task RollbackSavepointAsync(string savepointName, CancellationToken cancellationToken = default)
{
_logger.TransactionIgnoredWarning();
return Task.CompletedTask;
}

/// <inheritdoc />
public virtual void ReleaseSavepoint(string savepointName)
=> _logger.TransactionIgnoredWarning();

/// <inheritdoc />
public virtual Task ReleaseSavepointAsync(string savepointName, CancellationToken cancellationToken = default)
{
_logger.TransactionIgnoredWarning();
return Task.CompletedTask;
}

/// <inheritdoc />
public virtual bool AreSavepointsSupported => true;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
80 changes: 80 additions & 0 deletions src/EFCore.Relational/Storage/RelationalConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,86 @@ public virtual Task RollbackTransactionAsync(CancellationToken cancellationToken
return CurrentTransaction.RollbackAsync(cancellationToken);
}

/// <inheritdoc />
public virtual void CreateSavepoint(string savepointName)
{
if (CurrentTransaction == null)
{
throw new InvalidOperationException(RelationalStrings.NoActiveTransaction);
}

CurrentTransaction.Save(savepointName);
}

/// <inheritdoc />
public virtual Task CreateSavepointAsync(string savepointName, CancellationToken cancellationToken = default)
{
if (CurrentTransaction == null)
{
throw new InvalidOperationException(RelationalStrings.NoActiveTransaction);
}

return CurrentTransaction.SaveAsync(savepointName, cancellationToken);
}

/// <inheritdoc />
public virtual void RollbackSavepoint(string savepointName)
{
if (CurrentTransaction == null)
{
throw new InvalidOperationException(RelationalStrings.NoActiveTransaction);
}

CurrentTransaction.Rollback(savepointName);
}

/// <inheritdoc />
public virtual Task RollbackSavepointAsync(string savepointName, CancellationToken cancellationToken = default)
{
if (CurrentTransaction == null)
{
throw new InvalidOperationException(RelationalStrings.NoActiveTransaction);
}

return CurrentTransaction.RollbackAsync(savepointName, cancellationToken);
}

/// <inheritdoc />
public virtual void ReleaseSavepoint(string savepointName)
{
if (CurrentTransaction == null)
{
throw new InvalidOperationException(RelationalStrings.NoActiveTransaction);
}

CurrentTransaction.Release(savepointName);
}

/// <inheritdoc />
public virtual Task ReleaseSavepointAsync(string savepointName, CancellationToken cancellationToken = default)
{
if (CurrentTransaction == null)
{
throw new InvalidOperationException(RelationalStrings.NoActiveTransaction);
}

return CurrentTransaction.ReleaseAsync(savepointName, cancellationToken);
}

/// <inheritdoc />
public virtual bool AreSavepointsSupported
{
get
{
if (CurrentTransaction == null)
{
throw new InvalidOperationException(RelationalStrings.NoActiveTransaction);
}

return CurrentTransaction.AreSavepointsSupported;
}
}

/// <summary>
/// Opens the connection to the database.
/// </summary>
Expand Down
75 changes: 62 additions & 13 deletions src/EFCore.Relational/Update/Internal/BatchExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ namespace Microsoft.EntityFrameworkCore.Update.Internal
/// </summary>
public class BatchExecutor : IBatchExecutor
{
private const string SavepointName = "__EFSavePoint";

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down Expand Up @@ -58,19 +60,28 @@ public virtual int Execute(
IRelationalConnection connection)
{
var rowsAffected = 0;
IDbContextTransaction startedTransaction = null;
var transaction = connection.CurrentTransaction;
var beganTransaction = false;
var createdSavepoint = false;
try
{
if (connection.CurrentTransaction == null
if (transaction == null
&& (connection as ITransactionEnlistmentManager)?.EnlistedTransaction == null
&& Transaction.Current == null
&& CurrentContext.Context.Database.AutoTransactionsEnabled)
{
startedTransaction = connection.BeginTransaction();
transaction = connection.BeginTransaction();
beganTransaction = true;
}
else
{
connection.Open();

if (transaction?.AreSavepointsSupported == true)
{
transaction.Save(SavepointName);
createdSavepoint = true;
}
}

foreach (var batch in commandBatches)
Expand All @@ -79,13 +90,29 @@ public virtual int Execute(
rowsAffected += batch.ModificationCommands.Count;
}

startedTransaction?.Commit();
if (beganTransaction)
{
transaction.Commit();
}
}
catch
{
if (createdSavepoint)
{
transaction.Rollback(SavepointName);
}

throw;
}
finally
{
if (startedTransaction != null)
if (createdSavepoint)
{
startedTransaction.Dispose();
transaction.Release(SavepointName);
}
else if (beganTransaction)
{
transaction.Dispose();
}
else
{
Expand All @@ -108,19 +135,28 @@ public virtual async Task<int> ExecuteAsync(
CancellationToken cancellationToken = default)
{
var rowsAffected = 0;
IDbContextTransaction startedTransaction = null;
var transaction = connection.CurrentTransaction;
var beganTransaction = false;
var createdSavepoint = false;
try
{
if (connection.CurrentTransaction == null
if (transaction == null
&& (connection as ITransactionEnlistmentManager)?.EnlistedTransaction == null
&& Transaction.Current == null
&& CurrentContext.Context.Database.AutoTransactionsEnabled)
{
startedTransaction = await connection.BeginTransactionAsync(cancellationToken);
transaction = await connection.BeginTransactionAsync(cancellationToken);
beganTransaction = true;
}
else
{
await connection.OpenAsync(cancellationToken);

if (transaction?.AreSavepointsSupported == true)
{
await transaction.SaveAsync(SavepointName, cancellationToken);
createdSavepoint = true;
}
}

foreach (var batch in commandBatches)
Expand All @@ -129,16 +165,29 @@ public virtual async Task<int> ExecuteAsync(
rowsAffected += batch.ModificationCommands.Count;
}

if (startedTransaction != null)
if (beganTransaction)
{
await startedTransaction.CommitAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
}
}
catch
{
if (createdSavepoint)
{
await transaction.RollbackAsync(SavepointName, cancellationToken);
}

throw;
}
finally
{
if (startedTransaction != null)
if (createdSavepoint)
{
await transaction.ReleaseAsync(SavepointName, cancellationToken);
}
else if (beganTransaction)
{
await startedTransaction.DisposeAsync();
await transaction.DisposeAsync();
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer([NotNull] this ISer
.TryAdd<IModelValidator, SqlServerModelValidator>()
.TryAdd<IProviderConventionSetBuilder, SqlServerConventionSetBuilder>()
.TryAdd<IUpdateSqlGenerator>(p => p.GetService<ISqlServerUpdateSqlGenerator>())
.TryAdd<IRelationalTransactionFactory, SqlServerTransactionFactory>()
.TryAdd<IModificationCommandBatchFactory, SqlServerModificationCommandBatchFactory>()
.TryAdd<IValueGeneratorSelector, SqlServerValueGeneratorSelector>()
.TryAdd<IRelationalConnection>(p => p.GetService<ISqlServerConnection>())
Expand Down
72 changes: 72 additions & 0 deletions src/EFCore.SqlServer/Storage/Internal/SqlServerTransaction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Storage;

namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal
{
public class SqlServerTransaction : RelationalTransaction, IDbContextTransaction
{
private readonly DbTransaction _dbTransaction;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public SqlServerTransaction(
[NotNull] IRelationalConnection connection,
[NotNull] DbTransaction transaction,
Guid transactionId,
[NotNull] IDiagnosticsLogger<DbLoggerCategory.Database.Transaction> logger,
bool transactionOwned)
: base(connection, transaction, transactionId, logger, transactionOwned)
=> _dbTransaction = transaction;

/// <inheritdoc />
public virtual void Save(string savepointName)
{
using var command = Connection.DbConnection.CreateCommand();
command.Transaction = _dbTransaction;
command.CommandText = "SAVE TRANSACTION " + savepointName;
command.ExecuteNonQuery();
}

/// <inheritdoc />
public virtual async Task SaveAsync(string savepointName, CancellationToken cancellationToken = default)
{
using var command = Connection.DbConnection.CreateCommand();
command.Transaction = _dbTransaction;
command.CommandText = "SAVE TRANSACTION " + savepointName;
await command.ExecuteNonQueryAsync(cancellationToken);
}

/// <inheritdoc />
public virtual void Rollback(string savepointName)
{
using var command = Connection.DbConnection.CreateCommand();
command.Transaction = _dbTransaction;
command.CommandText = "ROLLBACK TRANSACTION " + savepointName;
command.ExecuteNonQuery();
}

/// <inheritdoc />
public virtual async Task RollbackAsync(string savepointName, CancellationToken cancellationToken = default)
{
using var command = Connection.DbConnection.CreateCommand();
command.Transaction = _dbTransaction;
command.CommandText = "ROLLBACK TRANSACTION " + savepointName;
await command.ExecuteNonQueryAsync(cancellationToken);
}

/// <inheritdoc />
public virtual bool AreSavepointsSupported => true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Data.Common;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Storage;

namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal
{
public class SqlServerTransactionFactory : IRelationalTransactionFactory
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual RelationalTransaction Create(
IRelationalConnection connection, DbTransaction transaction, Guid transactionId, IDiagnosticsLogger<DbLoggerCategory.Database.Transaction> logger, bool transactionOwned)
=> new SqlServerTransaction(connection, transaction, transactionId, logger, transactionOwned);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public static IServiceCollection AddEntityFrameworkSqlite([NotNull] this IServic
.TryAdd<IModelValidator, SqliteModelValidator>()
.TryAdd<IProviderConventionSetBuilder, SqliteConventionSetBuilder>()
.TryAdd<IUpdateSqlGenerator, SqliteUpdateSqlGenerator>()
.TryAdd<IRelationalTransactionFactory, SqliteTransactionFactory>()
.TryAdd<IModificationCommandBatchFactory, SqliteModificationCommandBatchFactory>()
.TryAdd<IRelationalConnection>(p => p.GetService<ISqliteRelationalConnection>())
.TryAdd<IMigrationsSqlGenerator, SqliteMigrationsSqlGenerator>()
Expand Down
Loading

0 comments on commit 54e479e

Please sign in to comment.