この記事 を読んだ後、Dapperの使用方法を詳しく調べることにしました。
空のデータベースでこのコードを実行しました
var members = new List<Member>();
for (int i = 0; i < 50000; i++)
{
members.Add(new Member()
{
Username = i.toString(),
IsActive = true
});
}
using (var scope = new TransactionScope())
{
connection.Execute(@"
insert Member(Username, IsActive)
values(@Username, @IsActive)", members);
scope.Complete();
}
約20秒かかりました。これは2500挿入/秒です。悪いことではありませんが、ブログが1秒あたり45kの挿入を達成していることを考えると、素晴らしいことでもありません。 Dapperでこれを行うより効率的な方法はありますか?
また、副次的な注意として、このコードをVisual Studioデバッガーで実行すると、3分以上かかりました!私はそんなに見て本当に驚きました。
[〜#〜] update [〜#〜]
したがって、この
using (var scope = new TransactionScope())
{
connection.Execute(@"
insert Member(Username, IsActive)
values(@Username, @IsActive)", members);
scope.Complete();
}
この
connection.Execute(@"
insert Member(Username, IsActive)
values(@Username, @IsActive)", members);
両方とも20秒かかりました。
しかし、これには4秒かかりました!
SqlTransaction trans = connection.BeginTransaction();
connection.Execute(@"
insert Member(Username, IsActive)
values(@Username, @IsActive)", members, transaction: trans);
trans.Commit();
私が達成できた最高の方法は、このアプローチを使用して4秒で5万件のレコードを記録することでした。
SqlTransaction trans = connection.BeginTransaction();
connection.Execute(@"
insert Member(Username, IsActive)
values(@Username, @IsActive)", members, transaction: trans);
trans.Commit();
私は最近これに出くわし、接続が開かれた後にTransactionScopeが作成されることに気付きました(クエリとは異なり、Dappers Executeは接続を開かないため、これを想定しています)。ここでの回答Q4によると、 https://stackoverflow.com/a/2886326/455904 TransactionScopeによって接続が処理されることはありません。私の同僚はいくつかの簡単なテストを行い、TransactionScopeの外部で接続を開くとパフォーマンスが大幅に低下しました。
したがって、次のように変更しても機能するはずです。
// Assuming the connection isn't already open
using (var scope = new TransactionScope())
{
connection.Open();
connection.Execute(@"
insert Member(Username, IsActive)
values(@Username, @IsActive)", members);
scope.Complete();
}
Execute
メソッドを1つのinsertステートメントのみで使用すると、一括挿入が行われず、効率的でもありません。 Transaction
で受け入れられた回答でさえ、Bulk Insert
を行いません。
Bulk Insert
を実行する場合は、SqlBulkCopy
https://msdn.Microsoft.com/en-us/library/system.data.sqlclient.sqlbulkcopyを使用します
これより速いものは何も見つかりません。
免責事項:私はプロジェクトの所有者です Dapper Plus
このプロジェクトは無料ではありませんが、すべての一括操作を提供します。
(フードの下で使用SqlBulkCopy
)
そして、ID値の出力などのいくつかのオプション:
// CONFIGURE & MAP entity
DapperPlusManager.Entity<Order>()
.Table("Orders")
.Identity(x => x.ID);
// CHAIN & SAVE entity
connection.BulkInsert(orders)
.AlsoInsert(order => order.Items);
.Include(x => x.ThenMerge(order => order.Invoice)
.AlsoMerge(invoice => invoice.Items))
.AlsoMerge(x => x.ShippingAddress);
私たちのライブラリは複数のプロバイダーをサポートしています:
一括挿入を非常に迅速に実行できる拡張メソッドを作成しました。
public static class DapperExtensions
{
public static async Task BulkInsert<T>(
this IDbConnection connection,
string tableName,
IReadOnlyCollection<T> items,
Dictionary<string, Func<T, object>> dataFunc)
{
const int MaxBatchSize = 1000;
const int MaxParameterSize = 2000;
var batchSize = Math.Min((int)Math.Ceiling((double)MaxParameterSize / dataFunc.Keys.Count), MaxBatchSize);
var numberOfBatches = (int)Math.Ceiling((double)items.Count / batchSize);
var columnNames = dataFunc.Keys;
var insertSql = $"INSERT INTO {tableName} ({string.Join(", ", columnNames.Select(e => $"[{e}]"))}) VALUES ";
var sqlToExecute = new List<Tuple<string, DynamicParameters>>();
for (var i = 0; i < numberOfBatches; i++)
{
var dataToInsert = items.Skip(i * batchSize)
.Take(batchSize);
var valueSql = GetQueries(dataToInsert, dataFunc);
sqlToExecute.Add(Tuple.Create($"{insertSql}{string.Join(", ", valueSql.Item1)}", valueSql.Item2));
}
foreach (var sql in sqlToExecute)
{
await connection.ExecuteAsync(sql.Item1, sql.Item2, commandTimeout: int.MaxValue);
}
}
private static Tuple<IEnumerable<string>, DynamicParameters> GetQueries<T>(
IEnumerable<T> dataToInsert,
Dictionary<string, Func<T, object>> dataFunc)
{
var parameters = new DynamicParameters();
return Tuple.Create(
dataToInsert.Select(e => $"({string.Join(", ", GenerateQueryAndParameters(e, parameters, dataFunc))})"),
parameters);
}
private static IEnumerable<string> GenerateQueryAndParameters<T>(
T entity,
DynamicParameters parameters,
Dictionary<string, Func<T, object>> dataFunc)
{
var paramTemplateFunc = new Func<Guid, string>(guid => $"@p{guid.ToString().Replace("-", "")}");
var paramList = new List<string>();
foreach (var key in dataFunc)
{
var paramName = paramTemplateFunc(Guid.NewGuid());
parameters.Add(paramName, key.Value(entity));
paramList.Add(paramName);
}
return paramList;
}
}
次に、この拡張メソッドを使用するには、次のようなコードを記述します。
await dbConnection.BulkInsert(
"MySchemaName.MyTableName",
myCollectionOfItems,
new Dictionary<string, Func<MyObjectToInsert, object>>
{
{ "ColumnOne", u => u.ColumnOne },
{ "ColumnTwo", u => u.ColumnTwo },
...
});
これは非常に原始的であり、トランザクションやcommandTimeout値を渡すなど、改善の余地がありますが、私にとってはうまくいきます。
これらの例はすべて不完全でした。
以下は、使用後に接続を適切に閉じ、さらにこのスレッドの最新のより良い回答に基づいて、トランザクションスコープを正しく使用してExcecuteのパフォーマンスを向上させるコードです。
using (var scope = new TransactionScope())
{
Connection.Open();
Connection.Execute(sqlQuery, parameters);
scope.Complete();
}