2011-12-16 12 views
5

So che questo è possibile in LINQ-to-SQL e ho visto frammenti che mi hanno portato a credere che sia possibile in EF. Esiste un'estensione là fuori che può fare qualcosa di simile:Codice EF Elimina prima lotto da IQueryable <T>?

var peopleQuery = Context.People.Where(p => p.Name == "Jim"); 

peopleQuery.DeleteBatch(); 

Dove DeleteBatch solo raccoglie parte il peopleQuery e crea una singola istruzione SQL per eliminare tutti i record appropriati, quindi esegue la query direttamente invece di marcare tutti coloro entità per la cancellazione e facendole fare una per una. Ho pensato di aver trovato qualcosa del genere nel codice qui sotto, ma fallisce immediatamente perché l'istanza non può essere castata in ObjectSet. Qualcuno sa come risolvere il problema prima di lavorare con EF Code? O sai da qualche parte che ha un esempio di questo fatto?

public static IQueryable<T> DeleteBatch<T>(this IQueryable<T> instance) where T : class 
{ 
    ObjectSet<T> query = instance as ObjectSet<T>; 
    ObjectContext context = query.Context; 

    string sqlClause = GetClause<T>(instance); 
    context.ExecuteStoreCommand("DELETE {0}", sqlClause); 

    return instance; 
} 

public static string GetClause<T>(this IQueryable<T> clause) where T : class 
{ 
    string snippet = "FROM [dbo].["; 

    string sql = ((ObjectQuery<T>)clause).ToTraceString(); 
    string sqlFirstPart = sql.Substring(sql.IndexOf(snippet)); 

    sqlFirstPart = sqlFirstPart.Replace("AS [Extent1]", ""); 
    sqlFirstPart = sqlFirstPart.Replace("[Extent1].", ""); 

    return sqlFirstPart; 
} 

risposta

5

Il framework Entity non supporta le operazioni batch. Mi piace il modo in cui il codice risolve il problema ma anche quello che fa esattamente quello che vuoi (ma per l'API ObjectContext) è una soluzione sbagliata.

Perché è la soluzione sbagliata?

Funziona solo in alcuni casi. Non funzionerà sicuramente in nessuna soluzione di mappatura avanzata in cui l'entità viene mappata su più tabelle (divisione dell'entità, eredità TPT). Sono quasi sicuro che tu possa trovare altre situazioni in cui non funzionerà a causa della complessità della query.

Mantiene contesto e database incoerenti. Questo è un problema di qualsiasi SQL eseguito su DB ma in questo caso l'SQL è nascosto e un altro programmatore che usa il tuo codice può non vederlo. Se elimini un record che è nello stesso tempo caricato nell'istanza di contesto, l'entità non verrà contrassegnata come eliminata e rimossa dal contesto (a meno che non si aggiunga quel codice al tuo metodo DeleteBatch - questo sarà particolarmente complicato se le mappe cancellate effettivamente mappano a più entità (divisione tabella)).

Il problema più importante è la modifica della query SQL generata da EF e le ipotesi che si stanno facendo su quella query. Si prevede che EF indicherà la prima tabella utilizzata nella query come Extent1. Sì, usa davvero quel nome ora ma è un'implementazione EF interna. Può cambiare in qualsiasi aggiornamento minore di EF. La creazione di una logica personalizzata attorno a interni di qualsiasi API è considerata una cattiva pratica.

Come risultato, è già necessario lavorare con la query a livello SQL in modo da poter richiamare la query SQL direttamente come mostrato da @mreyeros ed evitare i rischi in questa soluzione. Dovrai gestire nomi reali di tabelle e colonne ma è qualcosa che puoi controllare (la tua mappatura può definirli).

Se non si considerano questi rischi significativi si possono fare piccole modifiche al codice per farlo funzionare in DbContext API:

public static class DbContextExtensions 
{ 
    public static void DeleteBatch<T>(this DbContext context, IQueryable<T> query) where T : class 
    { 
     string sqlClause = GetClause<T>(query); 
     context.Database.ExecuteSqlCommand(String.Format("DELETE {0}", sqlClause)); 
    } 

    private static string GetClause<T>(IQueryable<T> clause) where T : class 
    { 
     string snippet = "FROM [dbo].["; 

     string sql = clause.ToString(); 
     string sqlFirstPart = sql.Substring(sql.IndexOf(snippet)); 

     sqlFirstPart = sqlFirstPart.Replace("AS [Extent1]", ""); 
     sqlFirstPart = sqlFirstPart.Replace("[Extent1].", ""); 

     return sqlFirstPart; 
    } 
} 

Ora si chiamerà lotto eliminare in questo modo:

context.DeleteBatch(context.People.Where(p => p.Name == "Jim")); 
+0

Sono d'accordo con te al 100% su tutti i tuoi punti, ma è un rischio accettabile nel nostro progetto considerando il serio colpo alla performance preso nel modo ufficiale. Grazie per la risposta! – Ocelot20

1

Non credo che le operazioni batch, come eliminare sono ancora supportate da EF. È possibile eseguire una query non elaborata:

context.Database.ExecuteSqlCommand("delete from dbo.tbl_Users where isActive = 0"); 
+0

so che non sono attualmente supportate, ma speravo che avrei potuto costruisci quel comando cancella direttamente dall'espressione come ero stato in grado di fare in passato. – Ocelot20

1

Nel caso in cui qualcun altro stia cercando questa funzionalità, ho usato alcuni dei commenti di Ladislav per migliorare il suo esempio. Come ha detto, con la soluzione originale, quando chiami SaveChanges(), se il contesto stava già monitorando una delle entità che hai eliminato, chiamerà la propria eliminazione.Questo non modifica alcun record e EF lo considera un problema di concorrenza e genera un'eccezione. Il metodo di seguito è più lento dell'originale poiché deve prima eseguire una query per gli elementi da eliminare, ma non scriverà una singola query di eliminazione per ogni entità eliminata che rappresenta il reale vantaggio in termini di prestazioni. Stacca tutte le entità che sono state interrogate, quindi se qualcuno di loro è già stato rintracciato, saprà di non cancellarli più.

public static void DeleteBatch<T>(this DbContext context, IQueryable<T> query) where T : LcmpTableBase 
{ 
    IEnumerable<T> toDelete = query.ToList(); 

    context.Database.ExecuteSqlCommand(GetDeleteCommand(query)); 

    var toRemove = context.ChangeTracker.Entries<T>().Where(t => t.State == EntityState.Deleted).ToList(); 

    foreach (var t in toRemove) 
     t.State = EntityState.Detached; 
} 

Ho cambiato anche questa parte per usare un'espressione regolare quando ho scoperto che c'era un tempo indeterminato spazi prossimità FROM porzione. Ho anche lasciato "[Extent1]" in là perché la query DELETE scritto nel modo originale non poteva gestire le query con join interni:

public static string GetDeleteCommand<T>(this IQueryable<T> clause) where T : class 
{ 
    string sql = clause.ToString(); 

    Match match = Regex.Match(sql, @"FROM\s*\[dbo\].", RegexOptions.IgnoreCase); 

    return string.Format("DELETE [Extent1] {0}", sql.Substring(match.Index)); 
} 
Problemi correlati