Bulk delete v3
Dans mes deux posts précédents (Bulk Delete avec EF4 et Bulk Delete sur les entités déjà chargées dans le cache), j’ai réalisé un POC pour faire du Bulk delete avec EF4.
Cependant, il y avait plusieurs problèmes avec la version précédente que je vais essayer de résoudre dans cette version :
- Frans m’a expliqué que ma solution était limité à peu de providers et qu’elle pouvait être optimisée. Il m’a conseillé d’utiliser la syntaxe “DELETE FROM FROM”.
- Danny a remarqué qu’il y avait un problème avec l’ordre des requêtes. Pour le corriger, j’exécute désormais mes bulk deletes avant mon SaveChanges afin de pouvoir ajouter un nouvel enregistrement sans risquer de le “perdre” lors de l’exécution du Bulk delete. De plus, dans ma précédente solution, on pouvait avoir de ce fait un cache désynchronisé.
- J’ai aussi identifié des problèmes avec les scenarii de mapping TPT, TPC, Vertical Entity Splitting, Horizontal Entity Splitting (quand plusieurs table sont mappées sur la même entité) ainsi que des problèmes avec le Table Splitting.
- J’ai également identifié un bug avec ma clause join dans le cas où les propriétés de la clé de l’entité auraient des noms différents de celui des colonnes de la table.
Avant de commencer, je voudrais remercier Frans et Danny pour leurs remarques / conseils.
Ci-dessous ma nouvelle version.
Et pour une fois, j’explique un peu mon code (après).
public static class ObjectSetExtension
{
public static void Delete<TBase, T>(this ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate = null)
where T : class, TBase
where TBase : class
{
IObjectContextWithBulkOperations context = entitySet.Context as IObjectContextWithBulkOperations;
if (context == null)
throw new NotImplementedException();
if (predicate == null)
predicate = item => true;
context.Delete(entitySet, predicate);
}
public static void DeleteLoadedEntities<TBase, T>(this ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate = null)
where T : class, TBase
where TBase : class
{
IObjectContextWithBulkOperations context = entitySet.Context as IObjectContextWithBulkOperations;
if (context == null)
throw new NotImplementedException();
if (predicate == null)
predicate = item => true;
context.DeleteLoadedEntities(entitySet, predicate);
}
public static void Delete<T>(this ObjectSet<T> entitySet, Expression<Func<T, bool>> predicate = null)
where T : class
{
Delete<T, T>(entitySet, predicate);
}
public static void DeleteLoadedEntities<T>(this ObjectSet<T> entitySet, Expression<Func<T, bool>> predicate = null)
where T : class
{
DeleteLoadedEntities<T, T>(entitySet, predicate);
}
}
public interface IObjectContextWithBulkOperations
{
void Delete<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate)
where T : class, TBase
where TBase : class;
void DeleteLoadedEntities<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate)
where T : class, TBase
where TBase : class;
}
public class ObjectContextWithBulkOperations : ObjectContext, IObjectContextWithBulkOperations
{
public ObjectContextWithBulkOperations(EntityConnection connection)
: base(connection)
{
OnContextCreated();
}
public ObjectContextWithBulkOperations(string connectionString)
: base(connectionString)
{
OnContextCreated();
}
protected ObjectContextWithBulkOperations(EntityConnection connection, string defaultContainerName)
: base(connection, defaultContainerName)
{
OnContextCreated();
}
protected ObjectContextWithBulkOperations(string connectionString, string defaultContainerName)
: base(connectionString, defaultContainerName)
{
OnContextCreated();
}
private void OnContextCreated()
{
ObjectMaterialized += NorthwindEntities_ObjectMaterialized;
}
private List<Action> _bulkDeletedActions;
private List<Action> BulkDeletedActions
{
get
{
if (_bulkDeletedActions == null)
_bulkDeletedActions = new List<Action>();
return _bulkDeletedActions;
}
}
private List<object> _bulkDeletedEntities;
public List<object> BulkDeletedEntities
{
get
{
if (_bulkDeletedEntities == null)
_bulkDeletedEntities = new List<object>();
return _bulkDeletedEntities;
}
}
private Dictionary<Type, List<Func<object, bool>>> _bulkDeletedFuncs;
public Dictionary<Type, List<Func<object, bool>>> BulkDeletedFuncs
{
get
{
if (_bulkDeletedFuncs == null)
_bulkDeletedFuncs = new Dictionary<Type, List<Func<object, bool>>>();
return _bulkDeletedFuncs;
}
}
public void Delete<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate)
where T : class, TBase
where TBase : class
{
Delete<TBase, T>(entitySet, predicate, true);
}
public void DeleteLoadedEntities<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate)
where T : class, TBase
where TBase : class
{
if ((predicate = CalculatePredicate(entitySet, predicate)) != null)
Delete<TBase, T>(entitySet, predicate, false);
}
private Expression<Func<T, bool>> CalculatePredicate<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> oldPredicate)
where T : class, TBase
where TBase : class
{
IEnumerable<PropertyInfo> keyMembers = entitySet.EntitySet.ElementType.KeyMembers.Select(km => typeof(T).GetProperty(km.Name)).ToList();
IEnumerable<T> entitiesEnumerable = ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified | EntityState.Unchanged)
.Select(ose => ose.Entity)
.OfType<T>();
ParameterExpression parameter = oldPredicate.Parameters.Single();
if (!entitiesEnumerable.Any())
return null;
return Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(
oldPredicate.Body,
entitiesEnumerable.Select(e =>
keyMembers.Select(km =>
Expression.Equal(
Expression.MakeMemberAccess(parameter, km),
Expression.Constant(km.GetValue(e, null))))
.Aggregate((accumulate, clause) =>
Expression.AndAlso(accumulate, clause)))
.Aggregate((accumulate, clause) =>
Expression.OrElse(accumulate, clause)))
, oldPredicate.Parameters);
}
private void Delete<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate, bool propagateToFutureEntities)
where TBase : class
where T : class, TBase
{
ObjectQuery<T> objectQuery = (ObjectQuery<T>)entitySet.OfType<T>().Where(predicate);
string selectSQLQuery = objectQuery.ToTraceString();
List<KeyValuePair<string, List<string>>> froms = new List<KeyValuePair<string, List<string>>>();
Match fromMatch = Regex.Match(entitySet.OfType<T>().ToTraceString(), "(FROM|JOIN)[ ]+((\\[[^\\]]+\\]).)*\\[([^\\]]+)\\]");
List<AssociationType> ssdlAsscociations = MetadataWorkspace.GetItems(DataSpace.SSpace).OfType<AssociationType>().ToList();
string firstFrom = null;
while (fromMatch.Success)
{
string fromValue = fromMatch.Groups[4].Value;
if (Regex.IsMatch(selectSQLQuery, string.Format("(FROM|JOIN)[ ]+((\\[[^\\]]+\\]).)*\\[{0}\\]", fromValue)))
{
var index = (from ssdlAssociation in ssdlAsscociations
where ssdlAssociation.ReferentialConstraints.Any(rc => fromValue == rc.ToProperties.First().DeclaringType.Name)
from table in froms.Select((f, i) => new { Table = f, Index = i })
where ssdlAssociation.ReferentialConstraints.Any(rc => table.Table.Key == rc.FromProperties.First().DeclaringType.Name)
orderby table.Index
select new { Index = table.Index, SSDLAssociation = ssdlAssociation, FKs = table.Table }).FirstOrDefault();
if (index != null)
froms.Insert(index.Index, new KeyValuePair<string, List<string>>(fromValue, (from fk in index.FKs.Value
let referentailConstraint = index.SSDLAssociation.ReferentialConstraints.First(rc => index.FKs.Key == rc.FromProperties.First().DeclaringType.Name)
select referentailConstraint.ToProperties.ElementAt(referentailConstraint.FromProperties.Select((p, pIndex) => new { p.Name, Index = pIndex }).First(p => p.Name == fk).Index).Name).ToList()));
else
{
if (firstFrom == null)
firstFrom = fromValue;
froms.Add(new KeyValuePair<string, List<string>>(fromValue, MetadataWorkspace.GetItems(DataSpace.SSpace).OfType<EntityType>().First(et => et.Name == fromValue).KeyMembers.Select(km => km.Name).ToList()));
}
}
fromMatch = fromMatch.NextMatch();
}
StringBuilder delete = new StringBuilder();
string selectSQLQueryWithoutSelect = selectSQLQuery.Substring(selectSQLQuery.IndexOf("FROM"));
IEnumerator<EdmMember> keyMembersEnumerator = null;
if (froms.Count > 1)
{
delete.Append("declare @DeleteIds table (");
StringBuilder keys = new StringBuilder();
keyMembersEnumerator = MetadataWorkspace.GetItems(DataSpace.SSpace).OfType<EntityType>().
First(et => et.Name == firstFrom).KeyMembers.ToList().GetEnumerator();
keyMembersEnumerator.MoveNext();
for (; ; )
{
string keyName = keyMembersEnumerator.Current.Name;
keys.Append(keyName);
delete.Append(keyName);
delete.Append(" ");
delete.Append(keyMembersEnumerator.Current.TypeUsage.EdmType.Name);
Facet maxLength = keyMembersEnumerator.Current.TypeUsage.Facets.FirstOrDefault(f => f.Name == "MaxLength");
if (maxLength != null)
{
delete.Append("(");
delete.Append(maxLength.Value);
delete.Append(")");
}
if (keyMembersEnumerator.MoveNext())
{
keys.Append(", ");
delete.Append(", ");
}
else
break;
}
delete.Append(");\n");
delete.Append("INSERT INTO @DeleteIds SELECT ");
delete.Append(keys.ToString());
delete.Append(" ");
delete.Append(selectSQLQueryWithoutSelect.Replace("@p__linq__", "@p"));
delete.Append(";\n");
}
foreach (KeyValuePair<string, List<string>> from in froms)
{
delete.Append("DELETE FROM [");
delete.Append(from.Key);
delete.Append("] FROM ");
if (froms.Count > 1)
{
delete.Append("[");
delete.Append(from.Key);
delete.Append("]");
delete.Append("INNER JOIN @deleteIds D ON ");
keyMembersEnumerator.Reset();
keyMembersEnumerator.MoveNext();
int index = 0;
for (; ; )
{
delete.Append("[");
delete.Append(from.Key);
delete.Append("].");
delete.Append(from.Value[index++]);
delete.Append(" = D.");
delete.Append(keyMembersEnumerator.Current);
if (keyMembersEnumerator.MoveNext())
delete.Append(" AND ");
else
break;
}
}
else
delete.Append(selectSQLQueryWithoutSelect.Substring(4).TrimStart());
delete.Append(";\n");
}
BulkDeletedActions.Add(() => ExecuteStoreCommand(delete.ToString(), objectQuery.Parameters.Select(p => p.Value).ToArray()));
Func<T, bool> predicateCompiled = predicate.Compile();
Func<object, bool> predicateCompiledObject = o =>
{
T t = o as T;
if (t == null)
return false;
return predicateCompiled(t);
};
if (propagateToFutureEntities)
{
List<Func<object, bool>> bulkDeletedFuncs;
if (BulkDeletedFuncs.TryGetValue(typeof(TBase), out bulkDeletedFuncs))
bulkDeletedFuncs.Add(predicateCompiledObject);
else
BulkDeletedFuncs.Add(typeof(TBase), new List<Func<object, bool>>() { predicateCompiledObject });
}
EntityType entityType = MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityType>().First(et => et.Name == typeof(T).Name);
var oneToOneSubEntityTypes = (from np in entityType.NavigationProperties
where np.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One && np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One
let otherEntityType = np.ToEndMember.GetEntityType()
let otherNavigationProperty = otherEntityType.NavigationProperties.FirstOrDefault(otherNP => otherNP.RelationshipType == np.RelationshipType)
select new
{
EntityType = otherEntityType,
ClrType = typeof(T).GetProperty(np.Name).PropertyType,
OtherNavigationPropertyName = otherNavigationProperty == null ? null : otherNavigationProperty.Name,
ReferencialConstraint = ((AssociationType)np.RelationshipType).ReferentialConstraints.FirstOrDefault()
}).ToList();
foreach (var subEntityTypeLoop in oneToOneSubEntityTypes)
{
var subEntityType = subEntityTypeLoop;
if (subEntityType.OtherNavigationPropertyName != null)
{
List<string> entityTypeKeys, subEntityTypeKeys;
if (subEntityType.ReferencialConstraint.FromProperties.First().DeclaringType == entityType)
{
entityTypeKeys = subEntityType.ReferencialConstraint.FromProperties.Select(p => p.Name).ToList();
subEntityTypeKeys = subEntityType.ReferencialConstraint.ToProperties.Select(p => p.Name).ToList();
}
else
{
entityTypeKeys = subEntityType.ReferencialConstraint.ToProperties.Select(p => p.Name).ToList();
subEntityTypeKeys = subEntityType.ReferencialConstraint.FromProperties.Select(p => p.Name).ToList();
}
ParameterExpression entityParameter = Expression.Parameter(typeof(object), "entity");
ParameterExpression subEntityParameter = Expression.Parameter(typeof(object), "subEntity");
Func<object, object, bool> associateToBulkEntities =
Expression.Lambda<Func<object, object, bool>>(
entityTypeKeys.Select((entityTypeKey, keyIndex) =>
Expression.Equal(
Expression.MakeMemberAccess(
Expression.Convert(
subEntityParameter,
subEntityType.ClrType),
subEntityType.ClrType.GetProperty(subEntityTypeKeys[keyIndex])),
Expression.MakeMemberAccess(
Expression.Convert(
entityParameter,
typeof(T)),
typeof(T).GetProperty(entityTypeKey)))).
Aggregate((accumulate, keyPredicate) => Expression.AndAlso(accumulate, keyPredicate)),
entityParameter,
subEntityParameter).
Compile();
Func<object, bool> npPredicate = subE => BulkDeletedEntities.OfType<T>().Any(e => associateToBulkEntities(e, subE));
List<Func<object, bool>> bulkDeletedFuncs;
if (BulkDeletedFuncs.TryGetValue(subEntityType.ClrType, out bulkDeletedFuncs))
bulkDeletedFuncs.Add(npPredicate);
else
BulkDeletedFuncs.Add(subEntityType.ClrType, new List<Func<object, bool>>() { npPredicate });
}
}
foreach (var entity in ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified | EntityState.Unchanged).
Select(ose => new { Entity = ose.Entity as T, ose.State }).
Where(e => e.Entity != null && predicateCompiled(e.Entity)))
{
if (entity.State != EntityState.Deleted)
DeleteObjectAndAddThemIntoBulkDeletedEntities(entity.Entity);
else
{
BulkDeletedEntities.Add(entity.Entity);
foreach (var subEntity in oneToOneSubEntityTypes.
SelectMany(subEntityType =>
ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified | EntityState.Unchanged).
Where(ose => subEntityType.ClrType.IsAssignableFrom(ose.Entity.GetType()) && !BulkDeletedEntities.Contains(ose.Entity))))
ApplyBulkDeletedFuncs(subEntity.Entity, subEntity.State);
}
}
}
private void NorthwindEntities_ObjectMaterialized(object sender, ObjectMaterializedEventArgs e)
{
ApplyBulkDeletedFuncs(e.Entity, EntityState.Unchanged);
}
private void ApplyBulkDeletedFuncs(object entity, EntityState entityState)
{
List<Func<object, bool>> bulkDeletedFuncs;
if (_bulkDeletedFuncs != null)
{
Type t = entity.GetType();
do
{
if (BulkDeletedFuncs.TryGetValue(t, out bulkDeletedFuncs))
foreach (Func<object, bool> bulkDeletedFunc in bulkDeletedFuncs)
if (bulkDeletedFunc(entity))
{
if (entityState != EntityState.Deleted)
DeleteObjectAndAddThemIntoBulkDeletedEntities(entity);
else
BulkDeletedEntities.Add(entity);
return;
}
} while ((t = t.BaseType) != null);
}
}
private void DeleteObjectAndAddThemIntoBulkDeletedEntities(object entity)
{
CollectionChangeEventHandler objectStateManagerObjectStateManagerChanged = (sender, e) => BulkDeletedEntities.Add(e.Element);
ObjectStateManager.ObjectStateManagerChanged += objectStateManagerObjectStateManagerChanged;
DeleteObject(entity);
ObjectStateManager.ObjectStateManagerChanged -= objectStateManagerObjectStateManagerChanged;
BulkDeletedEntities.Add(entity);
}
public override int SaveChanges(SaveOptions options)
{
int value;
using (TransactionScope transaction = new TransactionScope())
{
if (_bulkDeletedActions != null)
foreach (Action action in _bulkDeletedActions)
action();
if (_bulkDeletedEntities != null)
foreach (object entity in _bulkDeletedEntities)
{
ObjectStateEntry ose;
if (ObjectStateManager.TryGetObjectStateEntry(entity, out ose))
Detach(entity);
}
value = base.SaveChanges(options);
transaction.Complete();
BulkDeletedActions.Clear();
BulkDeletedEntities.Clear();
BulkDeletedFuncs.Clear();
}
return value;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
ObjectMaterialized -= NorthwindEntities_ObjectMaterialized;
}
}
Maintenant, je vais “essayer” d’expliquer ce que j’ai fait.
Bulk delete implique un delete SQL sur plusieurs rows en une seule requête. Ceci n’est pas supporté par EF4 qui génère un delete par row.
Mon idée est donc de générer et d’exécuter moi-même la commande delete SQL.
J’ai donc ajouter des méthodes Delete sur mon ObjectContextWithBulkOperations qui hérite d’ObjectConext avec la possibilité de filtrer le delete sur des sous-types et d’ajouter des conditions au delete. Je voulais aussi être capable d’appliquer le delete seulement sur les entités déjà chargées (avec seulement une seule commande DB). J’ai quatre méthodes pour cela.
Ensuite, comme c’est plus pratique d’avoir les méthodes sur la classe ObjectSet<T>, j’ai rajouté quatre extension methods qui rappellent elles-même les méthodes d’ObjectContextWithBulkOperations.
Maintenant passons à la classe la plus importante : ObjectContextWithBulkOperations.
Quand la méthode Delete est appelée il faut :
- compléter le prédicat pour ajouter une condition sur les clés si le bulk delete s’effectue seulement sur les entités chargées dans le cache
- déterminer la commande SQL delete
- enregistrer les commandes delete à exécuter dans la méthode SaveChanges
- passer les entités qui seront supprimées à l’état supprimé
- stocker le moyen de déterminer les entités qui seront supprimées. Ainsi, je serai capable de supprimer automatiquement les entités qui seront chargées après l’appel de mon Delete (avant le SaveChanges) // si on ne veux pas se limiter aux entités déjà chargées dans le cache
Quand on appelle la méthode SaveChanges, il faut :
- démarrer une transcation
- exécuter toutes les commandes SQL delete
- détacher toutes les entités supprimées par une des commandes delete
- appeler la méthode SaveChanges du framework
- fermer la transaction.
Compléter le prédicat pour ajouter une condition sur les clés si le bulk delete est seulement sur les entités chargées dans le cache
Je voulais utiliser la méthode Contains mais comme la clé de l’entité peut contenir plusieurs propriété, ce n’est pas possible. J’utilise donc des Or entre chaque entité et des And entre chaque propriété.
private Expression<Func<T, bool>> CalculatePredicate<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> oldPredicate)
where T : class, TBase
where TBase : class
{
IEnumerable<PropertyInfo> keyMembers = entitySet.EntitySet.ElementType.KeyMembers.Select(km => typeof(T).GetProperty(km.Name)).ToList();
IEnumerable<T> entitiesEnumerable = ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified | EntityState.Unchanged)
.Select(ose => ose.Entity)
.OfType<T>();
ParameterExpression parameter = oldPredicate.Parameters.Single();
if (!entitiesEnumerable.Any())
return null;
return Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(
oldPredicate.Body,
entitiesEnumerable.Select(e =>
keyMembers.Select(km =>
Expression.Equal(
Expression.MakeMemberAccess(parameter, km),
Expression.Constant(km.GetValue(e, null))))
.Aggregate((accumulate, clause) =>
Expression.AndAlso(accumulate, clause)))
.Aggregate((accumulate, clause) =>
Expression.OrElse(accumulate, clause)))
, oldPredicate.Parameters);
}
Déterminer la commande SQL delete :
La principale difficulté vient du fait que l’on n’accède pas aux metadata du mapping au runtime avec EF4. Mon idée est donc de générer une requête SQL select à partir de l’ObjectSet pour déterminer les tables.
Avec plusieurs scenarii de mapping (TPT, TPC, Vertical Entity Splitting, Horizontal Entity Splitting), un EntitySet est mappé sur plusieurs tables. Il faut donc déterminer les requêtes delete à exécuter sur chaque table et l’odre de suppression afin d’éviter les FK constraint violations.
Comment procéder pour parser la requête SQL générée ?
Tout d’abord, je vais parser la requête générée par entitySet.OfType<T> sans condition pour éviter l’impact du where sur la requête généré et donc sur le parse. Ensuite, je vérifie que la table est toujours présente dans la requête avec le where. En effet dans le cas de l’Horizontal Entity Splitting ou du TPC, la clause where peut éliminer certaines tables.
Si j’ai plusieurs tables, je dois générer plusieurs commandes SQL (dans le bon ordre). Dans chaque scénario de mapping avec plusieurs tables, toutes les tables “ont le même type de clé”. Je commence donc par insérer dans une table temporaire les pks des rows qui vont être supprimés et ensuite, je peux exécuter toutes les commandes SQL delete en faisant un inner join sur ma table temporaire. // Ceci risque de casser la compatibilité avec certains providers de base de données.
ObjectQuery<T> objectQuery = (
ObjectQuery<T>)entitySet.OfType<T>().Where(predicate);
string selectSQLQuery = objectQuery.ToTraceString();
List<
KeyValuePair<
string,
List<
string>>> froms =
new List<
KeyValuePair<
string,
List<
string>>>();
Match fromMatch =
Regex.Match(entitySet.OfType<T>().ToTraceString(),
"(FROM|JOIN)[ ]+((\\[[^\\]]+\\]).)*\\[([^\\]]+)\\]");
List<
AssociationType> ssdlAsscociations = MetadataWorkspace.GetItems(
DataSpace.SSpace).OfType<
AssociationType>().ToList();
string firstFrom =
null;
while (fromMatch.Success)
{
string fromValue = fromMatch.Groups[4].Value;
if (
Regex.IsMatch(selectSQLQuery,
string.Format(
"(FROM|JOIN)[ ]+((\\[[^\\]]+\\]).)*\\[{0}\\]", fromValue)))
{
var index = (
from ssdlAssociation
in ssdlAsscociations
where ssdlAssociation.ReferentialConstraints.Any(rc => fromValue == rc.ToProperties.First().DeclaringType.Name)
from table
in froms.Select((f, i) =>
new { Table = f, Index = i })
where ssdlAssociation.ReferentialConstraints.Any(rc => table.Table.Key == rc.FromProperties.First().DeclaringType.Name)
orderby table.Index
select new { Index = table.Index, SSDLAssociation = ssdlAssociation, FKs = table.Table }).FirstOrDefault();
if (index !=
null)
froms.Insert(index.Index,
new KeyValuePair<
string,
List<
string>>(fromValue, (
from fk
in index.FKs.Value
let referentailConstraint = index.SSDLAssociation.ReferentialConstraints.First(rc => index.FKs.Key == rc.FromProperties.First().DeclaringType.Name)
select referentailConstraint.ToProperties.ElementAt(referentailConstraint.FromProperties.Select((p, pIndex) =>
new { p.Name, Index = pIndex }).First(p => p.Name == fk).Index).Name).ToList()));
else
{
if (firstFrom ==
null)
firstFrom = fromValue;
froms.Add(
new KeyValuePair<
string,
List<
string>>(fromValue, MetadataWorkspace.GetItems(
DataSpace.SSpace).OfType<
EntityType>().First(et => et.Name == fromValue).KeyMembers.Select(km => km.Name).ToList()));
}
}
fromMatch = fromMatch.NextMatch();
}
StringBuilder delete =
new StringBuilder();
string selectSQLQueryWithoutSelect = selectSQLQuery.Substring(selectSQLQuery.IndexOf(
"FROM"));
IEnumerator<
EdmMember> keyMembersEnumerator =
null;
if (froms.Count > 1)
{
delete.Append(
"declare @DeleteIds table (");
StringBuilder keys =
new StringBuilder();
keyMembersEnumerator = MetadataWorkspace.GetItems(
DataSpace.SSpace).OfType<
EntityType>().
First(et => et.Name == firstFrom).KeyMembers.ToList().GetEnumerator();
keyMembersEnumerator.MoveNext();
for (; ; )
{
string keyName = keyMembersEnumerator.Current.Name;
keys.Append(keyName);
delete.Append(keyName);
delete.Append(
" ");
delete.Append(keyMembersEnumerator.Current.TypeUsage.EdmType.Name);
Facet maxLength = keyMembersEnumerator.Current.TypeUsage.Facets.FirstOrDefault(f => f.Name ==
"MaxLength");
if (maxLength !=
null)
{
delete.Append(
"(");
delete.Append(maxLength.Value);
delete.Append(
")");
}
if (keyMembersEnumerator.MoveNext())
{
keys.Append(
", ");
delete.Append(
", ");
}
else
break;
}
delete.Append(
");\n");
delete.Append(
"INSERT INTO @DeleteIds SELECT ");
delete.Append(keys.ToString());
delete.Append(
" ");
delete.Append(selectSQLQueryWithoutSelect.Replace(
"@p__linq__",
"@p"));
delete.Append(
";\n");
}
foreach (
KeyValuePair<
string,
List<
string>> from
in froms)
{
delete.Append(
"DELETE FROM [");
delete.Append(from.Key);
delete.Append(
"] FROM ");
if (froms.Count > 1)
{
delete.Append(
"[");
delete.Append(from.Key);
delete.Append(
"]");
delete.Append(
"INNER JOIN @deleteIds D ON ");
keyMembersEnumerator.Reset();
keyMembersEnumerator.MoveNext();
int index = 0;
for (; ; )
{
delete.Append(
"[");
delete.Append(from.Key);
delete.Append(
"].");
delete.Append(from.Value[index++]);
delete.Append(
" = D.");
delete.Append(keyMembersEnumerator.Current);
if (keyMembersEnumerator.MoveNext())
delete.Append(
" AND ");
else
break;
}
}
else
delete.Append(selectSQLQueryWithoutSelect.Substring(4).TrimStart());
delete.Append(
";\n");
}
Enregistrer les commandes delete à exécuter dans la méthode SaveChanges
J’utilise ma liste BulkDeletedActions pour stocker les actions qui exécuteront les commandes SQL delete quand elles seront appelées.
BulkDeletedActions.Add(() => ExecuteStoreCommand(delete.ToString(), objectQuery.Parameters.Select(p => p.Value).ToArray()));
Passer les entités qui seront supprimées à l’état supprimé
Pour déterminer les entités supprimées, je compile l’Expression du prédicat de mon Bulk delete pour avoir un Func et être capable de l’exécuter sur mes entités présentes dans mon cache.
foreach (
var entity
in ObjectStateManager.GetObjectStateEntries(
EntityState.Added |
EntityState.Deleted |
EntityState.Modified |
EntityState.Unchanged).
Select(ose =>
new { Entity = ose.Entity
as T, ose.State }).
Where(e => e.Entity !=
null && predicateCompiled(e.Entity)))
{
if (entity.State !=
EntityState.Deleted)
DeleteObjectAndAddThemIntoBulkDeletedEntities(entity.Entity);
Avec le Table Splitting, je peux avoir plusieurs entités mappées sur la même table. Il faut donc que je supprime chacune d’elles. Du fait que j’ai une relation one to one dans le CSDL, les entités liées seront automatiquement supprimées si je supprime l’entité et je dois juste les ajouter dans la liste BulkDeletedEntities (pour les détacher avant le SaveChanges).
private void DeleteObjectAndAddThemIntoBulkDeletedEntities(object entity)
{
CollectionChangeEventHandler objectStateManagerObjectStateManagerChanged = (sender, e) => BulkDeletedEntities.Add(e.Element);
ObjectStateManager.ObjectStateManagerChanged += objectStateManagerObjectStateManagerChanged;
DeleteObject(entity);
ObjectStateManager.ObjectStateManagerChanged -= objectStateManagerObjectStateManagerChanged;
BulkDeletedEntities.Add(entity);
}
Il y a cependant un problème si l’entité est déjà supprimée. Dans ce cas, le process est différent:
else
{
BulkDeletedEntities.Add(entity.Entity);
foreach (var subEntity in oneToOneSubEntityTypes.
SelectMany(subEntityType =>
ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified | EntityState.Unchanged).
Where(ose => subEntityType.ClrType.IsAssignableFrom(ose.Entity.GetType()) && !BulkDeletedEntities.Contains(ose.Entity))))
ApplyBulkDeletedFuncs(subEntity.Entity, subEntity.State);
}
}
avec ce code pour récupérer les entités associés en one to one
EntityType entityType = MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityType>().First(et => et.Name == typeof(T).Name);
var oneToOneSubEntityTypes = (from np in entityType.NavigationProperties
where np.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One && np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One
let otherEntityType = np.ToEndMember.GetEntityType()
let otherNavigationProperty = otherEntityType.NavigationProperties.FirstOrDefault(otherNP => otherNP.RelationshipType == np.RelationshipType)
select new
{
EntityType = otherEntityType,
ClrType = typeof(T).GetProperty(np.Name).PropertyType,
OtherNavigationPropertyName = otherNavigationProperty == null ? null : otherNavigationProperty.Name,
ReferencialConstraint = ((AssociationType)np.RelationshipType).ReferentialConstraints.FirstOrDefault()
}).ToList();
Stocker le moyen de déterminer les entités qui seront supprimées
Mon idée ici est d’utiliser mon Func et de l’utiliser dans la matérialisation de l’entité. Pour cela, the stocke toutes les actions par entité (uniquement dans le cas où je ne me réduit pas aux entités déjà chargées) :
List<Func<object, bool>> bulkDeletedFuncs;
if (BulkDeletedFuncs.TryGetValue(typeof(TBase), out bulkDeletedFuncs))
bulkDeletedFuncs.Add(predicateCompiledObject);
else
BulkDeletedFuncs.Add(typeof(TBase), new List<Func<object, bool>>() { predicateCompiledObject });
Là encore, le Table Splitting pose problème. En effet, imaginez que l’on charge une entité e1. Ensuite nous faisons un bulk delete sur l’entity set e1s : e1s.Delete(). Ensuite, je charge e2 qui a la même clé que e1. Je dois donc passer e2 dans l’état deleted et l’ajouter dans les BulkDeletedEntities.
Pour cela, quand je supprime une entité (avec un bulk delete), je stocke également les func pour ses sous-entités dans BulkDeletedFuncs. // Notez que j’utilise une Expression que je compile car c’est plus performant que d’utiliser la réflection.
foreach (var subEntityTypeLoop in oneToOneSubEntityTypes)
{
var subEntityType = subEntityTypeLoop;
if (subEntityType.OtherNavigationPropertyName != null)
{
List<string> entityTypeKeys, subEntityTypeKeys;
if (subEntityType.ReferencialConstraint.FromProperties.First().DeclaringType == entityType)
{
entityTypeKeys = subEntityType.ReferencialConstraint.FromProperties.Select(p => p.Name).ToList();
subEntityTypeKeys = subEntityType.ReferencialConstraint.ToProperties.Select(p => p.Name).ToList();
}
else
{
entityTypeKeys = subEntityType.ReferencialConstraint.ToProperties.Select(p => p.Name).ToList();
subEntityTypeKeys = subEntityType.ReferencialConstraint.FromProperties.Select(p => p.Name).ToList();
}
ParameterExpression entityParameter = Expression.Parameter(typeof(object), "entity");
ParameterExpression subEntityParameter = Expression.Parameter(typeof(object), "subEntity");
Func<object, object, bool> associateToBulkEntities =
Expression.Lambda<Func<object, object, bool>>(
entityTypeKeys.Select((entityTypeKey, keyIndex) =>
Expression.Equal(
Expression.MakeMemberAccess(
Expression.Convert(
subEntityParameter,
subEntityType.ClrType),
subEntityType.ClrType.GetProperty(subEntityTypeKeys[keyIndex])),
Expression.MakeMemberAccess(
Expression.Convert(
entityParameter,
typeof(T)),
typeof(T).GetProperty(entityTypeKey)))).
Aggregate((accumulate, keyPredicate) => Expression.AndAlso(accumulate, keyPredicate)),
entityParameter,
subEntityParameter).
Compile();
Func<object, bool> npPredicate = subE => BulkDeletedEntities.OfType<T>().Any(e => associateToBulkEntities(e, subE));
List<Func<object, bool>> bulkDeletedFuncs;
if (BulkDeletedFuncs.TryGetValue(subEntityType.ClrType, out bulkDeletedFuncs))
bulkDeletedFuncs.Add(npPredicate);
else
BulkDeletedFuncs.Add(subEntityType.ClrType, new List<Func<object, bool>>() { npPredicate });
}
}
Ensuite, quand l’entité est matérialisée, je regarde si elle doit être marquée supprimée, ce que je fais le cas échéant.
private void NorthwindEntities_ObjectMaterialized(object sender, ObjectMaterializedEventArgs e)
{
ApplyBulkDeletedFuncs(e.Entity, EntityState.Unchanged);
}
private void ApplyBulkDeletedFuncs(object entity, EntityState entityState)
{
List<Func<object, bool>> bulkDeletedFuncs;
if (_bulkDeletedFuncs != null)
{
Type t = entity.GetType();
do
{
if (BulkDeletedFuncs.TryGetValue(t, out bulkDeletedFuncs))
foreach (Func<object, bool> bulkDeletedFunc in bulkDeletedFuncs)
if (bulkDeletedFunc(entity))
{
if (entityState != EntityState.Deleted)
DeleteObjectAndAddThemIntoBulkDeletedEntities(entity);
else
BulkDeletedEntities.Add(entity);
return;
}
} while ((t = t.BaseType) != null);
}
}
Exécuter toutes les commandes SQL delete
Pour cela, il me suffit d’itérer sur les BulkDeletedActions et de les exécuter.
if (_bulkDeletedActions != null)
foreach (Action action in _bulkDeletedActions)
action();
Détacher toutes les entités deleted par une des commande delete
Comme les actions précédentes ont supprimées des rows, je dois détacher les entités associées pour éviter d’avoir des delete qui retourne 0 et donc une exception lors du SaveChanges.
if (_bulkDeletedEntities != null)
foreach (object entity in _bulkDeletedEntities)
{
ObjectStateEntry ose;
if (ObjectStateManager.TryGetObjectStateEntry(entity, out ose))
Detach(entity);
}
Appeler la méthode SaveChanges du framework
value = base.SaveChanges(options);
Ensuite, il me suffit de fermer ma transaction et de vider mes 3 listes / dictionnaires.
transaction.Complete();
BulkDeletedActions.Clear();
BulkDeletedEntities.Clear();
BulkDeletedFuncs.Clear();
Voilà. Avec cette version, je pense que je couvre maintenant tous les scenarii de mapping.
J’ai maintenant une vraie version du Bulk Delete pour SQL Server. //ça serait très simple de transformer mon code pour l’utiliser avec les autre providers.
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :