Skip to content

Commit

Permalink
Delete and insert seed data for dependents when principal is deleted …
Browse files Browse the repository at this point in the history
…and the FK is set to cascade

Fixes #15364
  • Loading branch information
AndriySvyryd committed Aug 10, 2019
1 parent 8bf8a4c commit 6c87dc6
Show file tree
Hide file tree
Showing 12 changed files with 422 additions and 72 deletions.
122 changes: 81 additions & 41 deletions src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
Expand Up @@ -8,6 +8,7 @@
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
Expand Down Expand Up @@ -51,6 +52,7 @@ public class MigrationsModelDiffer : IMigrationsModelDiffer

private IUpdateAdapter _sourceUpdateAdapter;
private IUpdateAdapter _targetUpdateAdapter;
private readonly List<SharedTableEntryMap<EntryMapping>> _sharedTableEntryMaps = new List<SharedTableEntryMap<EntryMapping>>();

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -375,7 +377,7 @@ public virtual IReadOnlyList<MigrationOperation> GetDifferences(IModel source, I
? Remove(source, diffContext)
: Enumerable.Empty<MigrationOperation>();

return schemaOperations.Concat(GetDataOperations());
return schemaOperations.Concat(GetDataOperations(diffContext));
}

private IEnumerable<MigrationOperation> DiffAnnotations(
Expand Down Expand Up @@ -1602,6 +1604,7 @@ protected virtual IEnumerable<MigrationOperation> Remove([NotNull] ISequence sou
}

_targetUpdateAdapter = UpdateAdapterFactory.CreateStandalone(target);
_targetUpdateAdapter.CascadeDeleteTiming = CascadeTiming.Never;

foreach (var targetEntityType in target.GetEntityTypes())
{
Expand All @@ -1620,14 +1623,17 @@ protected virtual IEnumerable<MigrationOperation> Remove([NotNull] ISequence sou
}

_sourceUpdateAdapter = UpdateAdapterFactory.CreateStandalone(source);
_sourceUpdateAdapter.CascadeDeleteTiming = CascadeTiming.OnSaveChanges;

foreach (var sourceEntityType in source.GetEntityTypes())
{
foreach (var sourceSeed in sourceEntityType.GetSeedData())
{
_sourceUpdateAdapter
.CreateEntry(sourceSeed, sourceEntityType)
.EntityState = EntityState.Added;
var entry = _sourceUpdateAdapter
.CreateEntry(sourceSeed, sourceEntityType);

entry.EntityState = EntityState.Added;
entry.EntityState = EntityState.Unchanged;
}
}
}
Expand Down Expand Up @@ -1713,6 +1719,7 @@ protected virtual IEnumerable<MigrationOperation> Remove([NotNull] ISequence sou
source.Name,
source.Schema)
((t, s, c) => new EntryMapping());
_sharedTableEntryMaps.Add(sourceTableEntryMappingMap);

foreach (var sourceEntityType in source.EntityTypes)
{
Expand Down Expand Up @@ -1781,8 +1788,10 @@ protected virtual IEnumerable<MigrationOperation> Remove([NotNull] ISequence sou
targetEntry.EntityState = EntityState.Unchanged;
}

if (entryMapping.RecreateRow)
if (sourceEntry.EntityState == EntityState.Deleted
|| entryMapping.RecreateRow)
{
entryMapping.RecreateRow = true;
continue;
}

Expand Down Expand Up @@ -1852,30 +1861,6 @@ var modelValuesChanged
}
}
}

foreach (var entryMapping in sourceTableEntryMappingMap.Values)
{
if (entryMapping.RecreateRow
|| entryMapping.TargetEntries.Count == 0)
{
foreach (var targetEntry in entryMapping.TargetEntries)
{
targetEntry.EntityState = EntityState.Added;
}

foreach (var sourceEntry in entryMapping.SourceEntries)
{
sourceEntry.EntityState = EntityState.Deleted;
}
}
else
{
foreach (var sourceEntry in entryMapping.SourceEntries)
{
sourceEntry.EntityState = EntityState.Detached;
}
}
}
}

private static IUpdateEntry GetEntry(
Expand All @@ -1897,16 +1882,53 @@ var modelValuesChanged
/// 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>
protected virtual IEnumerable<MigrationOperation> GetDataOperations()
protected virtual IEnumerable<MigrationOperation> GetDataOperations([NotNull] DiffContext diffContext)
{
if (_sourceUpdateAdapter != null)
foreach (var sourceTableEntryMappingMap in _sharedTableEntryMaps)
{
foreach (var sourceEntry in _sourceUpdateAdapter.Entries.Where(e => e.EntityState == EntityState.Added).ToList())
foreach (var entryMapping in sourceTableEntryMappingMap.Values)
{
sourceEntry.EntityState = EntityState.Detached;
if (entryMapping.RecreateRow
|| entryMapping.TargetEntries.Count == 0)
{
foreach (var sourceEntry in entryMapping.SourceEntries)
{
sourceEntry.EntityState = EntityState.Deleted;
_sourceUpdateAdapter.CascadeDelete(
sourceEntry,
sourceEntry.EntityType.GetReferencingForeignKeys()
.Where(
fk =>
{
var behavior = diffContext.FindTarget(fk)?.DeleteBehavior;
return behavior != null && behavior != DeleteBehavior.ClientNoAction;
}));
}
}
}
}

foreach (var sourceTableEntryMappingMap in _sharedTableEntryMaps)
{
foreach (var entryMapping in sourceTableEntryMappingMap.Values)
{
if (entryMapping.SourceEntries.Any(e => e.EntityState == EntityState.Deleted))
{
foreach (var targetEntry in entryMapping.TargetEntries)
{
targetEntry.EntityState = EntityState.Added;
}

foreach (var sourceEntry in entryMapping.SourceEntries)
{
sourceEntry.EntityState = EntityState.Deleted;
}
}
}
}

_sharedTableEntryMaps.Clear();

foreach (var updateAdapter in new[] { _sourceUpdateAdapter, _targetUpdateAdapter })
{
if (updateAdapter == null)
Expand Down Expand Up @@ -1983,14 +2005,17 @@ protected virtual IEnumerable<MigrationOperation> GetDataOperations()
batchInsertOperation = null;
}

yield return new DeleteDataOperation
if (c.Entries.All(e => diffContext.FindDrop(e.EntityType.RootType()) == null))
{
Schema = c.Schema,
Table = c.TableName,
KeyColumns = c.ColumnModifications.Where(col => col.IsKey).Select(col => col.ColumnName).ToArray(),
KeyValues = ToMultidimensionalArray(
c.ColumnModifications.Where(col => col.IsKey).Select(GetValue).ToArray())
};
yield return new DeleteDataOperation
{
Schema = c.Schema,
Table = c.TableName,
KeyColumns = c.ColumnModifications.Where(col => col.IsKey).Select(col => col.ColumnName).ToArray(),
KeyValues = ToMultidimensionalArray(
c.ColumnModifications.Where(col => col.IsKey).Select(GetValue).ToArray())
};
}
}
}

Expand Down Expand Up @@ -2222,6 +2247,7 @@ protected class DiffContext
= new Dictionary<IEntityType, TableMapping>();

private readonly IDictionary<object, object> _targetToSource = new Dictionary<object, object>();
private readonly IDictionary<object, object> _sourceToTarget = new Dictionary<object, object>();

private readonly IDictionary<IEntityType, CreateTableOperation> _createTableOperations
= new Dictionary<IEntityType, CreateTableOperation>();
Expand Down Expand Up @@ -2288,7 +2314,10 @@ public DiffContext([CanBeNull] IModel source, [CanBeNull] IModel target)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void AddMapping<T>([NotNull] T source, [NotNull] T target)
=> _targetToSource.Add(target, source);
{
_targetToSource.Add(target, source);
_sourceToTarget.Add(source, target);
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -2378,6 +2407,17 @@ public virtual IProperty FindSource([NotNull] IProperty target)
return null;
}

/// <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 T FindTarget<T>([NotNull] T source)
=> _sourceToTarget.TryGetValue(source, out var target)
? (T)target
: default;

/// <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
14 changes: 11 additions & 3 deletions src/EFCore/ChangeTracking/ChangeTracker.cs
Expand Up @@ -127,7 +127,11 @@ public virtual QueryTrackingBehavior QueryTrackingBehavior
/// for required relationships.
/// </para>
/// </summary>
public virtual CascadeTiming DeleteOrphansTiming { get; set; } = CascadeTiming.Immediate;
public virtual CascadeTiming DeleteOrphansTiming
{
get => StateManager.DeleteOrphansTiming;
set => StateManager.DeleteOrphansTiming = value;
}

/// <summary>
/// <para>
Expand All @@ -141,7 +145,11 @@ public virtual QueryTrackingBehavior QueryTrackingBehavior
/// for required relationships.
/// </para>
/// </summary>
public virtual CascadeTiming CascadeDeleteTiming { get; set; } = CascadeTiming.Immediate;
public virtual CascadeTiming CascadeDeleteTiming
{
get => StateManager.CascadeDeleteTiming;
set => StateManager.CascadeDeleteTiming = value;
}

/// <summary>
/// Gets an <see cref="EntityEntry" /> for each entity being tracked by the context.
Expand Down Expand Up @@ -211,7 +219,7 @@ public virtual bool HasChanges()
/// </summary>
public virtual void DetectChanges()
{
if (_model[Internal.ChangeDetector.SkipDetectChangesAnnotation] == null)
if ((string)_model[Internal.ChangeDetector.SkipDetectChangesAnnotation] != "true")
{
ChangeDetector.DetectChanges(StateManager);
}
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore/ChangeTracking/CollectionEntry.cs
Expand Up @@ -60,7 +60,7 @@ private void LocalDetectChanges()
var targetType = Metadata.GetTargetType();
var context = InternalEntry.StateManager.Context;
var changeDetector = context.ChangeTracker.AutoDetectChangesEnabled
&& context.Model[ChangeDetector.SkipDetectChangesAnnotation] == null
&& (string)context.Model[ChangeDetector.SkipDetectChangesAnnotation] != "true"
? context.GetDependencies().ChangeDetector
: null;
foreach (var entity in collection.OfType<object>().ToList())
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore/ChangeTracking/EntityEntry.cs
Expand Up @@ -98,7 +98,7 @@ public virtual EntityState State
/// </summary>
public virtual void DetectChanges()
{
if (Context.Model[ChangeDetector.SkipDetectChangesAnnotation] == null)
if ((string)Context.Model[ChangeDetector.SkipDetectChangesAnnotation] != "true")
{
Context.GetDependencies().ChangeDetector.DetectChanges(InternalEntry);
}
Expand Down
20 changes: 18 additions & 2 deletions src/EFCore/ChangeTracking/Internal/IStateManager.cs
Expand Up @@ -41,6 +41,22 @@ public interface IStateManager : IResettableService
/// </summary>
StateManagerDependencies Dependencies { get; }

/// <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>
CascadeTiming DeleteOrphansTiming { get; set; }

/// <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>
CascadeTiming CascadeDeleteTiming { get; set; }

/// <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 @@ -325,7 +341,7 @@ IEnumerable<TEntity> GetNonDeletedEntities<TEntity>()
/// 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>
IList<IUpdateEntry> GetEntriesToSave();
IList<IUpdateEntry> GetEntriesToSave(bool cascadeChanges);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -437,7 +453,7 @@ IEnumerable<TEntity> GetNonDeletedEntities<TEntity>()
/// 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>
void CascadeDelete([NotNull] InternalEntityEntry entry, bool force);
void CascadeDelete([NotNull] InternalEntityEntry entry, bool force, IEnumerable<IForeignKey> foreignKeys = null);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
6 changes: 3 additions & 3 deletions src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
Expand Up @@ -327,7 +327,7 @@ private void SetEntityState(EntityState oldState, EntityState newState, bool acc

if ((newState == EntityState.Deleted
|| newState == EntityState.Detached)
&& StateManager.Context.ChangeTracker.CascadeDeleteTiming == CascadeTiming.Immediate)
&& StateManager.CascadeDeleteTiming == CascadeTiming.Immediate)
{
StateManager.CascadeDelete(this, force: false);
}
Expand Down Expand Up @@ -1130,7 +1130,7 @@ public virtual void AddToCollectionSnapshot([NotNull] IPropertyBase propertyBase
}

if (!isCascadeDelete
&& StateManager.Context.ChangeTracker.DeleteOrphansTiming == CascadeTiming.Immediate)
&& StateManager.DeleteOrphansTiming == CascadeTiming.Immediate)
{
HandleConceptualNulls(
StateManager.SensitiveLoggingEnabled,
Expand Down Expand Up @@ -1351,7 +1351,7 @@ public virtual void HandleConceptualNulls(bool sensitiveLoggingEnabled, bool for
if (cascadeFk != null
&& (force
|| (!isCascadeDelete
&& StateManager.Context.ChangeTracker.DeleteOrphansTiming != CascadeTiming.Never)))
&& StateManager.DeleteOrphansTiming != CascadeTiming.Never)))
{
var cascadeState = EntityState == EntityState.Added
? EntityState.Detached
Expand Down

0 comments on commit 6c87dc6

Please sign in to comment.