Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Abonnements

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 :

Publié samedi 22 mai 2010 01:01 par Matthieu MEZIL

Classé sous : , , ,

Commentaires

# re: Bulk delete v3 @ mercredi 26 mai 2010 13:55

J'espère que ta compagne m'excusera mais : Je T'aime !

J'adore tes articles ... c'est un peu sioux parfois à comprendre ... mais moi qui n'est pas trop le temps de me plonger dans les subtilités d'EF ... je prends un plaisir à les lire, les tester, les comprendre et après essayer mes propres expérimentations.

Merci Merci Merci

Erebuss

# re: Bulk delete v3 @ mercredi 26 mai 2010 21:08

Merci :D

Ca me change des "je vais vomir" de Florent ;)

Matthieu MEZIL

Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- SharePoint : Bug sur la gestion des permissions et la synchronisation Office par Blog Technique de Romelard Fabrice le 07-10-2014, 11:35

- SharePoint 2007 : La gestion des permissions pour les Workflows par Blog Technique de Romelard Fabrice le 07-08-2014, 11:27

- TypeMock: mock everything! par Fathi Bellahcene le 07-07-2014, 17:06

- Coding is like Read par Aurélien GALTIER le 07-01-2014, 15:30

- Mes vidéos autour des nouveautés VS 2013 par Fathi Bellahcene le 06-30-2014, 20:52

- Recherche un passionné .NET par Tkfé le 06-16-2014, 12:22

- [CodePlex] Projet KISS Workflow Foundation lancé par Blog de Jérémy Jeanson le 06-08-2014, 22:25

- Etes-vous yOS compatible ? (3/3) : la feuille de route par Le blog de Patrick [MVP SharePoint] le 06-06-2014, 00:30

- [MSDN] Utiliser l'approche Contract First avec Workflow Foundation 4.5 par Blog de Jérémy Jeanson le 06-05-2014, 21:19

- [ #ESPC14 ] TH10 Moving mountains with SharePoint ! par Le blog de Patrick [MVP SharePoint] le 06-01-2014, 11:30