Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix sproc TPH mapping #28875

Merged
merged 2 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public ColumnModificationParameters(
/// <param name="columnIsCondition">Indicates whether the column is used in the <c>WHERE</c> clause when updating.</param>
/// <param name="sensitiveLoggingEnabled">Indicates whether potentially sensitive data (e.g. database values) can be logged.</param>
public ColumnModificationParameters(
IUpdateEntry entry,
IUpdateEntry? entry,
IProperty? property,
IColumnBase column,
Func<string> generateParameterName,
Expand Down
87 changes: 61 additions & 26 deletions src/EFCore.Relational/Update/ModificationCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,6 @@ private List<IColumnModification> GenerateColumnModifications()

var nonMainEntry = !_mainEntryAdded || entry != _entries[0];

IEnumerable<IColumnMappingBase> columnMappings;
var optionalDependentWithAllNull = false;

if (StoreStoredProcedure is null)
Expand All @@ -371,25 +370,24 @@ private List<IColumnModification> GenerateColumnModifications()
continue;
}

columnMappings = tableMapping.ColumnMappings;

optionalDependentWithAllNull =
entry.EntityState is EntityState.Modified or EntityState.Added
&& tableMapping.Table.IsOptional(entry.EntityType)
&& tableMapping.Table.GetRowInternalForeignKeys(entry.EntityType).Any();

foreach (var columnMapping in tableMapping.ColumnMappings)
{
HandleColumnModification(columnMapping);
}
}
else
else // Stored procedure mapping case
{
var storedProcedureMapping = GetStoredProcedureMapping(entry.EntityType, EntityState);
Check.DebugAssert(storedProcedureMapping is not null, "No sproc mapping but StoredProcedure is not null");

columnMappings = storedProcedureMapping.ParameterMappings
.Concat((IEnumerable<IColumnMappingBase>)storedProcedureMapping.ResultColumnMappings);

// Stored procedures may have an additional rows affected parameter, result column or return value, which does not have a
// property/column mapping but still needs to have be represented via a column modification.
var storedProcedure = storedProcedureMapping.StoredProcedure;

// Stored procedures may have an additional rows affected result column or return value, which does not have a
// property/column mapping but still needs to have be represented via a column modification.
IColumnBase? rowsAffectedColumnBase = null;

if (storedProcedure.FindRowsAffectedParameter() is { } rowsAffectedParameter)
Expand All @@ -405,10 +403,12 @@ entry.EntityState is EntityState.Modified or EntityState.Added
rowsAffectedColumnBase = rowsAffectedReturnValue;
}

if (rowsAffectedColumnBase is not null)
// Add a column modification for rows affected result column/return value.
// A rows affected output parameter is added below in the correct position, with the rest of the parameters.
if (rowsAffectedColumnBase is IStoreStoredProcedureResultColumn or IStoreStoredProcedureReturnValue)
{
columnModifications.Add(CreateColumnModification(new ColumnModificationParameters(
entry,
entry: null,
property: null,
rowsAffectedColumnBase,
_generateParameterName!,
Expand All @@ -419,9 +419,56 @@ entry.EntityState is EntityState.Modified or EntityState.Added
columnIsCondition: false,
_sensitiveLoggingEnabled)));
}

// In TPH, the sproc has parameters for all entity types in the hierarchy; we must generate null column modifications
// for parameters for unrelated entity types.
// Enumerate over the sproc parameters in order, trying to match a corresponding parameter mapping.
// Note that we produce the column modifications in the same order as their sproc parameters; this is important and assumed
// later in the pipeline.
foreach (var parameter in StoreStoredProcedure.Parameters)
{
if (parameter.FindParameterMapping(entry.EntityType) is { } parameterMapping)
{
HandleColumnModification(parameterMapping);
continue;
}

// The parameter has no corresponding mapping; this is either a sibling property in a TPH hierarchy or a rows affected
// output parameter or return value.
columnModifications.Add(CreateColumnModification(new ColumnModificationParameters(
entry: null,
property: null,
parameter,
_generateParameterName!,
parameter.StoreTypeMapping,
valueIsRead: parameter.Direction.HasFlag(ParameterDirection.Output),
valueIsWrite: parameter.Direction.HasFlag(ParameterDirection.Input),
columnIsKey: false,
columnIsCondition: false,
_sensitiveLoggingEnabled)));
}

// Note that we only add column modifications for mapped result columns, even though the sproc may return additional result
// columns (e.g. for siblings in TPH). Our result propagation accesses result columns directly by their position.
foreach (var columnMapping in storedProcedureMapping.ResultColumnMappings)
{
HandleColumnModification(columnMapping);
}
}

foreach (var columnMapping in columnMappings)
if (optionalDependentWithAllNull && _logger != null)
{
if (_sensitiveLoggingEnabled)
{
_logger.OptionalDependentWithAllNullPropertiesWarningSensitive(entry);
}
else
{
_logger.OptionalDependentWithAllNullPropertiesWarning(entry);
}
}

void HandleColumnModification(IColumnMappingBase columnMapping)
{
var property = columnMapping.Property;
var column = columnMapping.Column;
Expand Down Expand Up @@ -488,7 +535,7 @@ entry.EntityState is EntityState.Modified or EntityState.Added
{
columnPropagator.ColumnModification.AddSharedColumnModification(columnModification);

continue;
return;
}

columnPropagator.ColumnModification = columnModification;
Expand All @@ -511,18 +558,6 @@ entry.EntityState is EntityState.Modified or EntityState.Added
optionalDependentWithAllNull = false;
}
}

if (optionalDependentWithAllNull && _logger != null)
{
if (_sensitiveLoggingEnabled)
{
_logger.OptionalDependentWithAllNullPropertiesWarningSensitive(entry);
}
else
{
_logger.OptionalDependentWithAllNullPropertiesWarning(entry);
}
}
}

return columnModifications;
Expand Down
23 changes: 4 additions & 19 deletions src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,25 +266,10 @@ public override void Complete(bool moreBatchesExpected)
/// <param name="modificationCommand">The modification command for which to add parameters.</param>
protected virtual void AddParameters(IReadOnlyModificationCommand modificationCommand)
{
IEnumerable<IColumnModification> columnModifications;

if (modificationCommand.StoreStoredProcedure is null)
{
columnModifications = modificationCommand.ColumnModifications;
}
else
{
if (modificationCommand.StoreStoredProcedure.ReturnValue is not null)
{
AddParameter(modificationCommand.ColumnModifications.First(c => c.Column is IStoreStoredProcedureReturnValue));
}

columnModifications = modificationCommand.ColumnModifications
.Where(c => c.Column is IStoreStoredProcedureParameter)
.OrderBy(c => ((IStoreStoredProcedureParameter)c.Column!).Position);
}

foreach (var columnModification in columnModifications)
Check.DebugAssert(!modificationCommand.ColumnModifications.Any(m => m.Column is IStoreStoredProcedureReturnValue)
|| modificationCommand.ColumnModifications[0].Column is IStoreStoredProcedureReturnValue,
"ResultValue column modification in non-first position");
foreach (var columnModification in modificationCommand.ColumnModifications)
{
AddParameter(columnModification);
}
Expand Down
15 changes: 9 additions & 6 deletions src/EFCore.Relational/Update/UpdateSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -355,13 +355,16 @@ public virtual ResultSetMapping AppendStoredProcedureCall(

// Only positional parameter style supported for now, see #28439

var orderedParameterModifications = command.ColumnModifications
.Where(c => c.Column is IStoreStoredProcedureParameter)
.OrderBy(c => ((IStoreStoredProcedureParameter)c.Column!).Position);

foreach (var columnModification in orderedParameterModifications)
// Note: the column modifications are already ordered according to the sproc parameter ordering
// (see ModificationCommand.GenerateColumnModifications)
for (var i = 0; i < command.ColumnModifications.Count; i++)
{
var parameter = (IStoreStoredProcedureParameter)columnModification.Column!;
var columnModification = command.ColumnModifications[i];

if (columnModification.Column is not IStoreStoredProcedureParameter parameter)
{
continue;
}

if (first)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -591,13 +591,16 @@ public override ResultSetMapping AppendStoredProcedureCall(

// Only positional parameter style supported for now, see #28439

var orderedParameterModifications = command.ColumnModifications
.Where(c => c.Column is IStoreStoredProcedureParameter)
.OrderBy(c => ((IStoreStoredProcedureParameter)c.Column!).Position);

foreach (var columnModification in orderedParameterModifications)
// Note: the column modifications are already ordered according to the sproc parameter ordering
// (see ModificationCommand.GenerateColumnModifications)
for (var i = 0; i < command.ColumnModifications.Count; i++)
{
var parameter = (IStoreStoredProcedureParameter)columnModification.Column!;
var columnModification = command.ColumnModifications[i];

if (columnModification.Column is not IStoreStoredProcedureParameter parameter)
{
continue;
}

if (first)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public DbSet<Entity> WithInputOutputParameterOnNonConcurrencyToken
=> Set<Entity>(nameof(WithInputOutputParameterOnNonConcurrencyToken));

public DbSet<TphParent> TphParent { get; set; }
public DbSet<TphChild> TphChild { get; set; }
public DbSet<TphChild1> TphChild { get; set; }
public DbSet<TptParent> TptParent { get; set; }
public DbSet<TptChild> TptChild { get; set; }
public DbSet<TptMixedParent> TptMixedParent { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel;

public class TphChild : TphParent
public class TphChild1 : TphParent
{
public int ChildProperty { get; set; }
public int Child1Property { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.TestModels.StoredProcedureUpdateModel;

public class TphChild2 : TphParent
{
public int Child2InputProperty { get; set; }
public int Child2OutputParameterProperty { get; set; }
public int Child2ResultColumnProperty { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
.HasRowsAffectedReturnValue());
});

modelBuilder.Entity<TphChild1>();

modelBuilder.Entity<TphChild2>(
b =>
{
b.Property(w => w.Child2OutputParameterProperty).HasDefaultValue(8);
b.Property(w => w.Child2ResultColumnProperty).HasDefaultValue(9);
});

modelBuilder.Entity<TphParent>(
b =>
{
Expand All @@ -191,11 +200,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
.HasParameter(w => w.Id, pb => pb.IsOutput())
.HasParameter("Discriminator")
.HasParameter(w => w.Name)
.HasParameter(nameof(TphChild.ChildProperty)));
.HasParameter(nameof(TphChild1.Child1Property))
.HasParameter(nameof(TphChild2.Child2InputProperty))
.HasParameter(nameof(TphChild2.Child2OutputParameterProperty), o => o.IsOutput())
.HasResultColumn(nameof(TphChild2.Child2ResultColumnProperty)));
});

modelBuilder.Entity<TphChild>().ToTable("Tph");

modelBuilder.Entity<TptParent>(
b =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ public virtual async Task Tph(bool async)
{
await using var context = CreateContext();

var entity1 = new TphChild { Name = "Child", ChildProperty = 8 };
var entity1 = new TphChild1 { Name = "Child", Child1Property = 8 };
context.TphChild.Add(entity1);
await SaveChanges(context, async);

Expand All @@ -525,7 +525,7 @@ public virtual async Task Tph(bool async)
var entity2 = context.TphChild.Single(b => b.Id == entity1.Id);

Assert.Equal("Child", entity2.Name);
Assert.Equal(8, entity2.ChildProperty);
Assert.Equal(8, entity2.Child1Property);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,15 @@ public override async Task Tph(bool async)
await base.Tph(async);

AssertSql(
@"@p0='1' (Direction = Output)
@p1='TphChild' (Nullable = false) (Size = 4000)
@"@p0=NULL (Nullable = false) (Direction = Output) (DbType = Int32)
@p1='TphChild1' (Nullable = false) (Size = 4000)
@p2='Child' (Size = 4000)
@p3='8' (Nullable = true)
@p4=NULL (DbType = Int32)
@p5=NULL (Direction = Output) (DbType = Int32)
SET NOCOUNT ON;
EXEC [Tph_Insert] @p0 OUTPUT, @p1, @p2, @p3;");
EXEC [Tph_Insert] @p0 OUTPUT, @p1, @p2, @p3, @p4, @p5 OUTPUT;");
}

public override async Task Tpt(bool async)
Expand Down Expand Up @@ -535,10 +537,13 @@ AS BEGIN
GO
CREATE PROCEDURE Tph_Insert(@Id int OUT, @Discriminator varchar(max), @Name varchar(max), @ChildProperty int)
CREATE PROCEDURE Tph_Insert(@Id int OUT, @Discriminator varchar(max), @Name varchar(max), @Child1Property int, @Child2InputProperty int, @Child2OutputParameterProperty int OUT)
AS BEGIN
INSERT INTO [Tph] ([Discriminator], [Name], [ChildProperty]) VALUES (@Discriminator, @Name, @ChildProperty);
DECLARE @Table table ([Child2OutputParameterProperty] int);
INSERT INTO [Tph] ([Discriminator], [Name], [Child1Property], [Child2InputProperty]) OUTPUT [Inserted].[Child2OutputParameterProperty] INTO @Table VALUES (@Discriminator, @Name, @Child1Property, @Child2InputProperty);
SET @Id = SCOPE_IDENTITY();
SELECT @Child2OutputParameterProperty = [Child2OutputParameterProperty] FROM @Table;
SELECT [Child2ResultColumnProperty] FROM [Tph] WHERE [Id] = @Id
END;
GO
Expand Down