Skip to content

Commit

Permalink
Don't use a temporary table for batching inserts.
Browse files Browse the repository at this point in the history
Part of #8415
Fixes #8322

asgasg
  • Loading branch information
AndriySvyryd committed Jun 9, 2017
1 parent 3cbae71 commit cbb6802
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 117 deletions.
121 changes: 57 additions & 64 deletions src/EFCore.SqlServer/Update/Internal/SqlServerUpdateSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ public virtual ResultSetMapping AppendBulkInsertOperation(
IReadOnlyList<ModificationCommand> modificationCommands,
int commandPosition)
{
Check.NotNull(commandStringBuilder, nameof(commandStringBuilder));
Check.NotEmpty(modificationCommands, nameof(modificationCommands));

if (modificationCommands.Count == 1
&& modificationCommands[0].ColumnModifications.All(o =>
!o.IsKey
Expand Down Expand Up @@ -140,7 +137,6 @@ private ResultSetMapping AppendBulkInsertWithoutServerValues(
}

private const string InsertedTableBaseName = "@inserted";
private const string ToInsertTableBaseName = "@toInsert";
private const string ToInsertTableAlias = "i";
private const string PositionColumnName = "_Position";
private const string PositionColumnDeclaration = "[" + PositionColumnName + "] [int]";
Expand All @@ -154,28 +150,6 @@ private ResultSetMapping AppendBulkInsertWithServerValues(
List<ColumnModification> keyOperations,
List<ColumnModification> readOperations)
{
AppendDeclareTable(
commandStringBuilder,
ToInsertTableBaseName,
commandPosition,
writeOperations,
PositionColumnDeclaration);

commandStringBuilder.Append("INSERT INTO ").Append(ToInsertTableBaseName).Append(commandPosition);
AppendValuesHeader(commandStringBuilder, writeOperations);
AppendValues(commandStringBuilder, writeOperations, "0");
for (var i = 1; i < modificationCommands.Count; i++)
{
commandStringBuilder.Append(",").AppendLine();
AppendValues(
commandStringBuilder,
modificationCommands[i].ColumnModifications.Where(o => o.IsWrite).ToList(),
i.ToString(CultureInfo.InvariantCulture));
}
commandStringBuilder
.AppendLine(SqlGenerationHelper.StatementTerminator)
.AppendLine();

AppendDeclareTable(
commandStringBuilder,
InsertedTableBaseName,
Expand All @@ -190,10 +164,10 @@ private ResultSetMapping AppendBulkInsertWithServerValues(
commandStringBuilder,
name,
schema,
ToInsertTableBaseName,
commandPosition,
ToInsertTableAlias,
writeOperations);
modificationCommands,
writeOperations,
PositionColumnName);
AppendOutputClause(
commandStringBuilder,
keyOperations,
Expand Down Expand Up @@ -239,39 +213,61 @@ private void AppendMergeCommandHeader(
[NotNull] StringBuilder commandStringBuilder,
[NotNull] string name,
[CanBeNull] string schema,
[NotNull] string toInsertTableName,
int toInsertTableIndex,
[NotNull] string toInsertTableAlias,
[NotNull] IReadOnlyList<ColumnModification> operations)
[NotNull] IReadOnlyList<ModificationCommand> modificationCommands,
[NotNull] IReadOnlyList<ColumnModification> writeOperations,
string additionalColumns = null)
{
Check.NotNull(commandStringBuilder, nameof(commandStringBuilder));
Check.NotEmpty(name, nameof(name));
Check.NotNull(operations, nameof(operations));

commandStringBuilder.Append("MERGE ");
SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, name, schema);

commandStringBuilder
.Append(" USING ")
.Append(toInsertTableName)
.Append(toInsertTableIndex.ToString(CultureInfo.InvariantCulture))
.Append(" AS ").Append(toInsertTableAlias).AppendLine(" ON 1=0")
.Append(" USING (");

AppendValuesHeader(commandStringBuilder, writeOperations);
AppendValues(commandStringBuilder, writeOperations, "0");
for (var i = 1; i < modificationCommands.Count; i++)
{
commandStringBuilder.Append(",").AppendLine();
AppendValues(
commandStringBuilder,
modificationCommands[i].ColumnModifications.Where(o => o.IsWrite).ToList(),
i.ToString(CultureInfo.InvariantCulture));
}

commandStringBuilder
.Append(") AS ").Append(toInsertTableAlias)
.Append(" (")
.AppendJoin(
writeOperations,
SqlGenerationHelper,
(sb, o, helper) => helper.DelimitIdentifier(sb, o.ColumnName));
if (additionalColumns != null)
{
commandStringBuilder
.Append(", ")
.Append(additionalColumns);
}

commandStringBuilder
.Append(")")
.AppendLine(" ON 1=0")
.AppendLine("WHEN NOT MATCHED THEN");

commandStringBuilder
.Append("INSERT ")
.Append("(")
.AppendJoin(
operations,
writeOperations,
SqlGenerationHelper,
(sb, o, helper) => { helper.DelimitIdentifier(sb, o.ColumnName); })
(sb, o, helper) => helper.DelimitIdentifier(sb, o.ColumnName))
.Append(")");

AppendValuesHeader(commandStringBuilder, operations);
AppendValuesHeader(commandStringBuilder, writeOperations);
commandStringBuilder
.Append("(")
.AppendJoin(
operations,
writeOperations,
toInsertTableAlias,
SqlGenerationHelper,
(sb, o, alias, helper) =>
Expand All @@ -287,9 +283,6 @@ private void AppendValues(
IReadOnlyList<ColumnModification> operations,
string additionalLiteral)
{
Check.NotNull(commandStringBuilder, nameof(commandStringBuilder));
Check.NotNull(operations, nameof(operations));

if (operations.Count > 0)
{
commandStringBuilder
Expand Down Expand Up @@ -370,16 +363,22 @@ private string GetTypeNameForCopy(IProperty property)
keyOrIndex: false,
size: null).StoreType;
}

return typeName ?? _typeMapper.FindMapping(property.ClrType).StoreType;
else
{
typeName = _typeMapper.FindMapping(property.ClrType).StoreType;
}
}
}

return property.ClrType == typeof(byte[])
&& (typeName.Equals("rowversion", StringComparison.OrdinalIgnoreCase)
|| typeName.Equals("timestamp", StringComparison.OrdinalIgnoreCase))
? (property.IsNullable ? "varbinary(8)" : "binary(8)")
: typeName;
if (property.ClrType == typeof(byte[])
&& typeName != null
&& (typeName.Equals("rowversion", StringComparison.OrdinalIgnoreCase)
|| typeName.Equals("timestamp", StringComparison.OrdinalIgnoreCase)))
{
return property.IsNullable ? "varbinary(8)" : "binary(8)";
}

return typeName;
}

// ReSharper disable once ParameterTypeCanBeEnumerable.Local
Expand Down Expand Up @@ -419,9 +418,6 @@ private ResultSetMapping AppendInsertOperationWithServerKeys(
IReadOnlyList<ColumnModification> readOperations,
int commandPosition)
{
Check.NotNull(commandStringBuilder, nameof(commandStringBuilder));
Check.NotNull(command, nameof(command));

var name = command.TableName;
var schema = command.Schema;
var operations = command.ColumnModifications;
Expand Down Expand Up @@ -455,7 +451,7 @@ private ResultSetMapping AppendSelectCommand(
.AppendJoin(
readOperations,
SqlGenerationHelper,
(sb, o, helper) => { helper.DelimitIdentifier(sb, o.ColumnName, "t"); })
(sb, o, helper) => helper.DelimitIdentifier(sb, o.ColumnName, "t"))
.Append(" FROM ");
SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, tableName, schema);
commandStringBuilder
Expand Down Expand Up @@ -495,7 +491,7 @@ private ResultSetMapping AppendSelectCommand(
/// </summary>
protected override ResultSetMapping AppendSelectAffectedCountCommand(StringBuilder commandStringBuilder, string name, string schema, int commandPosition)
{
Check.NotNull(commandStringBuilder, nameof(commandStringBuilder))
commandStringBuilder
.Append("SELECT @@ROWCOUNT")
.Append(SqlGenerationHelper.StatementTerminator).AppendLine()
.AppendLine();
Expand All @@ -508,7 +504,7 @@ protected override ResultSetMapping AppendSelectAffectedCountCommand(StringBuild
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public override void AppendBatchHeader(StringBuilder commandStringBuilder)
=> Check.NotNull(commandStringBuilder, nameof(commandStringBuilder))
=> commandStringBuilder
.Append("SET NOCOUNT ON")
.Append(SqlGenerationHelper.StatementTerminator).AppendLine();

Expand All @@ -518,9 +514,6 @@ public override void AppendBatchHeader(StringBuilder commandStringBuilder)
/// </summary>
protected override void AppendIdentityWhereCondition(StringBuilder commandStringBuilder, ColumnModification columnModification)
{
Check.NotNull(commandStringBuilder, nameof(commandStringBuilder));
Check.NotNull(columnModification, nameof(columnModification));

SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, columnModification.ColumnName);
commandStringBuilder.Append(" = ");

Expand All @@ -532,7 +525,7 @@ protected override void AppendIdentityWhereCondition(StringBuilder commandString
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
protected override void AppendRowsAffectedWhereCondition(StringBuilder commandStringBuilder, int expectedRowsAffected)
=> Check.NotNull(commandStringBuilder, nameof(commandStringBuilder))
=> commandStringBuilder
.Append("@@ROWCOUNT = ")
.Append(expectedRowsAffected.ToString(CultureInfo.InvariantCulture));
}
Expand Down
45 changes: 42 additions & 3 deletions test/EFCore.SqlServer.FunctionalTests/BatchingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,41 @@ public void Inserts_and_updates_are_batched_correctly()
}
}

[Fact]
public void Inserts_when_database_type_is_different()
{
using (var testStore = SqlServerTestStore.Create(DatabaseName + "_TypeMismatch"))
{
var options = new DbContextOptionsBuilder()
.UseSqlServer(testStore.Connection, b => b.ApplyConfiguration())
.UseInternalServiceProvider(
new ServiceCollection()
.AddEntityFrameworkSqlServer()
.BuildServiceProvider())
.Options;

using (var context = new BloggingContext(options))
{
context.Database.EnsureClean();
testStore.ExecuteNonQuery(@"
ALTER TABLE dbo.Owners
ALTER COLUMN Name nvarchar(MAX);");

var owner1 = new Owner { Id = "0", Name = "Zero" };
var owner2 = new Owner { Id = "A", Name = string.Join("", Enumerable.Repeat('A', 900)) };
context.Owners.Add(owner1);
context.Owners.Add(owner2);

context.SaveChanges();
}

using (var context = new BloggingContext(options))
{
Assert.Equal(2, context.Owners.Count());
}
}
}

private void AssertDatabaseState(bool clientOrder, List<Blog> expectedBlogs, DbContextOptions options)
{
expectedBlogs = clientOrder
Expand Down Expand Up @@ -171,7 +206,11 @@ public BloggingContext(DbContextOptions options)

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Owner>().Property(e => e.Version).IsConcurrencyToken().ValueGeneratedOnAddOrUpdate();
modelBuilder.Entity<Owner>(b =>
{
b.Property(e => e.Version).IsConcurrencyToken().ValueGeneratedOnAddOrUpdate();
b.Property(e => e.Name).HasColumnType("nvarchar(450)");
});
modelBuilder.Entity<Blog>(b =>
{
b.Property(e => e.Id).HasDefaultValueSql("NEWID()");
Expand All @@ -187,14 +226,14 @@ private class Blog
{
public Guid Id { get; set; }
public int Order { get; set; }
public int? OwnerId { get; set; }
public string OwnerId { get; set; }
public Owner Owner { get; set; }
public byte[] Version { get; set; }
}

public class Owner
{
public int Id { get; set; }
public string Id { get; set; }
public string Name { get; set; }
public byte[] Version { get; set; }
}
Expand Down
Loading

0 comments on commit cbb6802

Please sign in to comment.