Skip to content

Commit

Permalink
batch caching
Browse files Browse the repository at this point in the history
  • Loading branch information
mgravell committed Nov 21, 2023
1 parent 18bebf8 commit 752045e
Show file tree
Hide file tree
Showing 7 changed files with 357 additions and 162 deletions.
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<PackageVersion Include="Dapper.Advisor" Version="1.0.23" />
<PackageVersion Include="Dapper.StrongName" Version="2.1.21" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.6.133" />
<PackageVersion Include="Npgsql" Version="7.0.6" />
<PackageVersion Include="Npgsql" Version="8.0.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageVersion Include="Microsoft.Build" Version="17.7.2" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.7.2" />
Expand Down
75 changes: 68 additions & 7 deletions src/Dapper.AOT/CommandFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,26 @@ protected static bool TryRecycle(ref DbCommand? storage, DbCommand command)
command.Transaction = null;
return Interlocked.CompareExchange(ref storage, command, null) is null;
}


#if NET6_0_OR_GREATER
/// <summary>
/// Provides an opportunity to recycle and reuse batch instances
/// </summary>
protected static bool TryRecycle(ref DbBatch? storage, DbBatch batch)
{
// detach and recycle
batch.Connection = null;
batch.Transaction = null;
return Interlocked.CompareExchange(ref storage, batch, null) is null;
}

/// <summary>
/// Provides an opportunity to recycle and reuse batch instances
/// </summary>
public virtual bool TryRecycle(DbBatch batch) => false;
#endif

}

/// <summary>
Expand Down Expand Up @@ -190,8 +210,7 @@ public virtual DbCommand GetCommand(DbConnection connection, string sql, Command
internal void Initialize(in UnifiedCommand cmd,
string sql, CommandType commandType, T args)
{
cmd.CommandText = sql;
cmd.CommandType = commandType != 0 ? commandType : DapperAotExtensions.GetCommandType(sql);
cmd.SetCommand(sql, commandType != 0 ? commandType : DapperAotExtensions.GetCommandType(sql));
AddParameters(in cmd, args);
}

Expand All @@ -216,9 +235,10 @@ public virtual void AddParameters(in UnifiedCommand command, T args)
/// </summary>
public virtual void UpdateParameters(in UnifiedCommand command, T args)
{
if (command.Parameters.Count != 0) // try to avoid rogue "dirty" checks
var ps = command.Parameters;
if (ps.Count != 0) // try to avoid rogue "dirty" checks
{
command.Parameters.Clear();
ps.Clear();
}
AddParameters(in command, args);
}
Expand All @@ -234,10 +254,9 @@ public virtual void UpdateParameters(in UnifiedCommand command, T args)
// try to avoid any dirty detection in the setters
if (cmd.CommandText != sql) cmd.CommandText = sql;
if (cmd.CommandType != commandType) cmd.CommandType = commandType;
UpdateParameters(new(cmd), args);
UpdateParameters(new UnifiedCommand(cmd), args);
}
return cmd;

}

/// <summary>
Expand All @@ -257,10 +276,52 @@ public virtual void UpdateParameters(in UnifiedCommand command, T args)
#pragma warning restore IDE0079 // following will look unnecessary on up-level
public virtual bool UseBatch(string sql) => false;

#if NET6_0_OR_GREATER
/// <summary>
/// Create a populated batch from a command
/// </summary>
public virtual DbBatch GetBatch(DbConnection connection, string sql, CommandType commandType, T args)
{
Debug.Assert(connection.CanCreateBatch);
var batch = connection.CreateBatch();
batch.Connection = connection;
AddCommands(new(batch), sql, args);
return batch;
}

/// <summary>
/// Provides an opportunity to recycle and reuse batch instances
/// </summary>
protected DbBatch? TryReuse(ref DbBatch? storage, T args)
{
var batch = Interlocked.Exchange(ref storage, null);
if (batch is not null)
{
// try to avoid any dirty detection in the setters
UpdateParameters(new UnifiedBatch(batch), args);
}
return batch;
}
#endif


/// <summary>
/// Allows the caller to rewrite a composite command into a multi-command batch.
/// </summary>
public virtual void AddCommands(in UnifiedBatch batch, string sql, T args) => throw new NotSupportedException();
public virtual void AddCommands(in UnifiedBatch batch, string sql, T args)
{
// implement as basic mode
batch.SetCommand(sql, CommandType.Text);
AddParameters(in batch.Command, args);
}

/// <summary>
/// Allows the caller to update the parameter values of a multi-command batch.
/// </summary>
public virtual void UpdateParameters(in UnifiedBatch batch, T args)
{
UpdateParameters(in batch.Command, args);
}

/// <summary>
/// Allows an implementation to process output parameters etc after a multi-command batch has completed.
Expand Down
27 changes: 18 additions & 9 deletions src/Dapper.AOT/CommandT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,25 @@ private void GetUnifiedBatch(out UnifiedBatch batch, TArgs args) // it will most
{
if (commandType == CommandType.Text && commandFactory.UseBatch(sql))
{
batch = new UnifiedBatch(connection, transaction);
if (timeout >= 0)
#if NET6_0_OR_GREATER
if (connection.CanCreateBatch)
{
batch.TimeoutSeconds = timeout;
var dbBatch = commandFactory.GetBatch(connection, sql, commandType, args);
dbBatch.Connection = connection;
dbBatch.Timeout = timeout;
dbBatch.Transaction = transaction;
batch = new(dbBatch);
}
else
#endif
{
var cmd = connection.CreateCommand();
cmd.Connection = connection;
cmd.Transaction = transaction;
cmd.CommandTimeout = timeout;
batch = new(cmd);
commandFactory.AddCommands(in batch, sql, args);
}
commandFactory.AddCommands(in batch, sql, args);
}
else
{
Expand All @@ -118,11 +131,7 @@ internal void PostProcessAndRecycleUnified(ref SyncCommandState state, TArgs arg
{
Debug.Assert(state.Command is null); // all on unified command now
commandFactory.PostProcess(in state.UnifiedBatch.Command, args, rowCount);

if (state.UnifiedBatch.Command.Command is { } cmd && commandFactory.TryRecycle(cmd))
{
state.UnifiedBatch.Command.ClearSource();
}
state.UnifiedBatch.Command.TryRecycle(commandFactory);
}

internal void PostProcessAndRecycle(AsyncQueryState state, TArgs args, int rowCount)
Expand Down
18 changes: 17 additions & 1 deletion src/Dapper.AOT/UnifiedBatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -33,6 +32,15 @@ internal UnifiedBatch(DbCommand command)
Debug.Assert(Command.CommandCount == 1);
}

#if NET6_0_OR_GREATER
internal UnifiedBatch(DbBatch batch)
{
Command = new UnifiedCommand(batch);
commandStart = 0;
commandCount = batch.BatchCommands.Count;
Debug.Assert(Command.CommandCount > 0); // could be multiple for batch re-use scenarios
}
#endif

internal UnifiedBatch(DbConnection connection, DbTransaction? transaction)
{
Expand Down Expand Up @@ -91,13 +99,21 @@ private int GetCommandIndex(int localIndex)
public string CommandText
{
get => Command.CommandText;
[Obsolete("When possible, " + nameof(SetCommand) + " should be preferred", false)]
set => Command.CommandText = value;
}

/// <summary>
/// Initialize the <see cref="DbCommand.CommandText"/> and <see cref="DbCommand.CommandType"/>
/// </summary>
public void SetCommand(string commandText, CommandType commandType = CommandType.Text)
=> Command.SetCommand(commandText, commandType);

/// <inheritdoc cref="DbCommand.CommandType"/>
public CommandType CommandType
{
get => Command.CommandType;
[Obsolete("When possible, " + nameof(SetCommand) + " should be preferred", false)]
set => Command.CommandType = value;
}

Expand Down
99 changes: 76 additions & 23 deletions src/Dapper.AOT/UnifiedCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -104,6 +104,7 @@ public string CommandText
#endif
_ => "",
};
[Obsolete("When possible, " + nameof(SetCommand) + " should be preferred", false)]
set
{
switch (_source)
Expand Down Expand Up @@ -148,6 +149,7 @@ public CommandType CommandType
#endif
_ => CommandType.Text,
};
[Obsolete("When possible, " + nameof(SetCommand) + " should be preferred", false)]
set
{
switch (_source)
Expand Down Expand Up @@ -203,25 +205,31 @@ public int TimeoutSeconds
/// </summary>
public DbParameter AddParameter()
{
// TODO: optimize to avoid double tests
var p = CreateParameter();
Parameters.Add(p);
var p = CreateParameter(out var parameters);
parameters.Add(p);
return p;
}

/// <inheritdoc cref="DbCommand.CreateParameter"/>
public DbParameter CreateParameter()
public DbParameter CreateParameter() => CreateParameter(out _);

/// <inheritdoc cref="DbCommand.CreateParameter"/>
private DbParameter CreateParameter(out DbParameterCollection parameters)
{
switch (_source)
{
case DbCommand cmd:
parameters = cmd.Parameters;
return cmd.CreateParameter();
case List<DbCommand> list:
return list[_index].CreateParameter();
var activeCmd = list[_index];
parameters = activeCmd.Parameters;
return activeCmd.CreateParameter();
#if NET6_0_OR_GREATER
case DbBatch batch:
#if NET8_0_OR_GREATER // https://github.com/dotnet/runtime/issues/82326
var bc = batch.BatchCommands[_index];
parameters = bc.Parameters;
#if NET8_0_OR_GREATER // https://github.com/dotnet/runtime/issues/82326
if (bc.CanCreateParameter) return bc.CreateParameter();
#endif // NET8
return (_spareCommandForParameters ?? UnsafeBatchWithCommandForParameters()).CreateParameter();
Expand All @@ -246,12 +254,15 @@ internal UnifiedCommand(DbCommand command)

internal UnifiedCommand(DbBatch batch)
{
// withCommand is typically true for a ready-to-go command; it is false if, for example, we're
// doing a multi-row exec and want to start completely empty
_source = batch;
_spareCommandForParameters = null;

batch.BatchCommands.Add(batch.CreateBatchCommand());

var bc = batch.BatchCommands;
if (bc.Count == 0)
{
// initialize the first command
bc.Add(batch.CreateBatchCommand());
}
_index = 0;
}

Expand All @@ -264,8 +275,6 @@ private DbCommand UnsafeBatchWithCommandForParameters()
}
#endif

internal void ClearSource() => Unsafe.AsRef(in _source) = null!;

internal void PostProcess<T>(IEnumerable<T> source, CommandFactory<T> commandFactory)
{
var snapshot = _index;
Expand Down Expand Up @@ -461,19 +470,15 @@ internal DbDataReader ExecuteReader(CommandBehavior flags)

internal Task<int> ExecuteNonQueryAsync(CancellationToken cancellationToken)
{
switch (_source)
return _source switch
{
case DbCommand cmd:
return cmd.ExecuteNonQueryAsync(cancellationToken);
case List<DbCommand> list:
return ExecuteListAsync(list, cancellationToken);
DbCommand cmd => cmd.ExecuteNonQueryAsync(cancellationToken),
List<DbCommand> list => ExecuteListAsync(list, cancellationToken),
#if NET6_0_OR_GREATER
case DbBatch batch:
return batch.ExecuteNonQueryAsync(cancellationToken);
DbBatch batch => batch.ExecuteNonQueryAsync(cancellationToken),
#endif
default:
return TaskZero;
}
_ => TaskZero,
};

static async Task<int> ExecuteListAsync(List<DbCommand> list, CancellationToken cancellationToken)
{
Expand All @@ -486,5 +491,53 @@ static async Task<int> ExecuteListAsync(List<DbCommand> list, CancellationToken
}
}

/// <summary>
/// Initialize the <see cref="DbCommand.CommandText"/> and <see cref="DbCommand.CommandType"/>
/// </summary>
public void SetCommand(string commandText, CommandType commandType = CommandType.Text)
{
switch (_source)
{
// note we're trying to avoid triggering any unnecessary side-effects and
// cache-invalidations that could be triggered from setters
case DbCommand cmd:
if (cmd.CommandText != commandText) cmd.CommandText = commandText;
if (cmd.CommandType != commandType) cmd.CommandType = commandType;
break;
case List<DbCommand> list:
var activeCmd = list[_index];
if (activeCmd.CommandText != commandText) activeCmd.CommandText = commandText;
if (activeCmd.CommandType != commandType) activeCmd.CommandType = commandType;
break;
#if NET6_0_OR_GREATER
case DbBatch batch:
var bc = batch.BatchCommands[_index];
if (bc.CommandText != commandText) bc.CommandText = commandText;
if (bc.CommandType != commandType) bc.CommandType = commandType;
break;
#endif
}
}

internal void TryRecycle(CommandFactory commandFactory)
{
if (_source switch
{
// note we're trying to avoid triggering any unnecessary side-effects and
// cache-invalidations that could be triggered from setters
DbCommand cmd => commandFactory.TryRecycle(cmd),
// note we don't expect to recycle list usage in this way; we're only expecting
// single-arg scenarios
#if NET6_0_OR_GREATER
DbBatch batch => commandFactory.TryRecycle(batch),
#endif
_ => false,
})
{
// wipe the source - someone else can see it
Unsafe.AsRef(in _source) = null!;
}
}

private static readonly Task<int> TaskZero = Task.FromResult<int>(0);
}
Loading

0 comments on commit 752045e

Please sign in to comment.