2012-08-24 14 views
10

Questo è stato chiesto prima, ma nessuna soluzione descritta è abbastanza veloce per la mia app ha bisogno.Dati principali: cancella tutti gli oggetti di un tipo di entità, ovvero cancella una tabella

Nel protocollo di comunicazione che abbiamo impostato, il server invia una nuova serie di tutti i clienti ogni volta che viene eseguita una sincronizzazione. In precedenza, avevamo archiviato come plist. Ora voglio usare i dati principali.

Ci possono essere migliaia di voci. La cancellazione di ciascuna di esse richiede molto tempo. C'è un modo per eliminare tutte le righe in una determinata tabella in Core Data?

delete from customer 

Questa chiamata in sqlite avviene all'istante. Passare ciascuno di essi individualmente in Core Data può richiedere 30 secondi su un iPad1.

È ragionevole arrestare i dati di base, ovvero eliminare l'archivio di persistenza e tutti i contesti degli oggetti gestiti, quindi rilasciarli in sqlite ed eseguire il comando delete sulla tabella? Nessun'altra attività è in corso durante questo processo, quindi non ho bisogno di accedere ad altre parti del database.

+0

Un'alternativa è cambiare il modo in cui il protocollo di comunicazione funziona. Considera solo l'invio delle modifiche all'elenco dei tuoi clienti ai tuoi dispositivi. Vedi http://stackoverflow.com/questions/5035132/how-to-sync-iphone-core-data-with-web-server-and-then-push-to-other-devices/5052208#5052208 per una descrizione di come potresti fare questo – chris

+0

http://stackoverflow.com/a/5733105/2412290 Questo sito è una buona idea, rapporto di utilizzo entità superiore rimuovi sub – mosn

risposta

25

Dave DeLong è un esperto di, beh, quasi tutto, e quindi mi sento come se sto dicendo Gesù come camminare sulle acque. Certo, il suo post è del 2009, che è stato tanto tempo fa.

Tuttavia, l'approccio nel collegamento pubblicato da Bot non è necessariamente il modo migliore per gestire le eliminazioni di grandi dimensioni.

Fondamentalmente, quel post suggerisce di recuperare gli ID oggetto e quindi scorrere iterarli, chiamando delete su ogni oggetto.

Il problema è che quando si elimina un singolo oggetto, deve gestire anche tutte le relazioni associate, il che potrebbe causare ulteriori recuperi.

Quindi, se è necessario eseguire eliminazioni su larga scala come questa, suggerisco di adattare il database generale in modo da poter isolare le tabelle in specifici archivi di dati principali. In questo modo puoi semplicemente cancellare l'intero negozio ed eventualmente ricostruire i piccoli bit che vuoi conservare. Questo sarà probabilmente l'approccio più veloce.

Tuttavia, se si desidera eliminare gli oggetti stessi, si dovrebbe seguire questo modello ...

Fai i tuoi eliminazioni in lotti, all'interno di una piscina autorelease, ed essere sicuri di pre-fetch eventuali rapporti in cascata.Tutti questi, insieme, ridurranno al minimo il numero di volte che devi effettivamente andare al database e, quindi, ridurranno il tempo necessario per eseguire l'eliminazione.

Nell'approccio proposto, che scende a ...

  1. Fetch objectIds di tutti gli oggetti da cancellare
  2. scorrere l'elenco ed eliminare ogni oggetto

Se avere relazioni a cascata, si incontreranno molti viaggi extra nel database, e IO è veramente lento. Vuoi minimizzare il numero di volte che devi visitare il database.

Anche se inizialmente può sembrare controintuitivo, si desidera recuperare più dati di quanti si pensi di voler eliminare. Il motivo è che tutti i dati possono essere recuperati dal database in poche operazioni IO.

Quindi, sulla vostra richiesta di recupero, si desidera impostare ...

[fetchRequest setRelationshipKeyPathsForPrefetching:@[@"relationship1", @"relationship2", .... , @"relationship3"]]; 

in cui queste relazioni rappresentano tutte le relazioni che possono avere un'eliminazione a cascata regola.

Ora, quando il recupero è completo, si hanno tutti gli oggetti che verranno cancellati, oltre agli oggetti che verranno eliminati in seguito all'eliminazione di tali oggetti.

Se si dispone di una gerarchia complessa, si desidera eseguire il prefetch il più possibile in anticipo. Altrimenti, quando elimini un oggetto, Core Data dovrà andare a prendere ogni relazione individualmente per ogni oggetto in modo che possa gestire l'eliminazione a cascata.

Questo sprecherà un TON di tempo, perché di conseguenza si eseguiranno molte altre operazioni di I/O.

Ora, dopo che il recupero è stato completato, è possibile scorrere gli oggetti ed eliminarli. Per le eliminazioni di grandi dimensioni è possibile vedere un ordine di grandezza accelerare.

Inoltre, se si dispone di molti oggetti, suddividerlo in più lotti e farlo all'interno di un pool di rilascio automatico.

Infine, fai questo in un thread in background separato, in modo che l'interfaccia utente non penda. È possibile utilizzare un MOC separato, connesso a un coordinatore di negozio persistente e fare in modo che il MOC principale gestisca le notifiche di DidSave per rimuovere gli oggetti dal relativo contesto.

mentre questo appare come codice, trattarlo come pseudo-codice ...

NSManagedObjectContext *deleteContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateConcurrencyType]; 
// Get a new PSC for the same store 
deleteContext.persistentStoreCoordinator = getInstanceOfPersistentStoreCoordinator(); 

// Each call to performBlock executes in its own autoreleasepool, so we don't 
// need to explicitly use one if each chunk is done in a separate performBlock 
__block void (^block)(void) = ^{ 
    NSFetchRequest *fetchRequest = // 
    // Only fetch the number of objects to delete this iteration 
    fetchRequest.fetchLimit = NUM_ENTITIES_TO_DELETE_AT_ONCE; 
    // Prefetch all the relationships 
    fetchRequest.relationshipKeyPathsForPrefetching = prefetchRelationships; 
    // Don't need all the properties 
    fetchRequest.includesPropertyValues = NO; 
    NSArray *results = [deleteContext executeFetchRequest:fetchRequest error:&error]; 
    if (results.count == 0) { 
     // Didn't get any objects for this fetch 
     if (nil == results) { 
      // Handle error 
     } 
     return; 
    } 
    for (MyEntity *entity in results) { 
     [deleteContext deleteObject:entity]; 
    } 
    [deleteContext save:&error]; 
    [deleteContext reset]; 

    // Keep deleting objects until they are all gone 
    [deleteContext performBlock:block]; 
}; 

[deleteContext preformBlock:block]; 

Naturalmente, è necessario fare appropriata gestione degli errori, ma questo è l'idea di base.

Recupero in lotti se si dispone di così tanti dati da eliminare che creerà memoria. Non recuperare tutte le proprietà. Precarica le relazioni per ridurre al minimo le operazioni IO. Utilizzare autoreleasepool per evitare che la memoria si ingrandisca. Potare il contesto. Eseguire l'attività su un thread in background.

Se si dispone di un grafico molto complesso, assicurarsi di precaricare tutte le relazioni in cascata per tutte le entità dell'intero grafico dell'oggetto.

Nota, il tuo contesto principale dovrà gestire le notifiche di DidSave per mantenere il suo contesto al passo con le eliminazioni.

EDIT

Grazie. Un sacco di buoni punti. Tutto bene spiegato tranne, perché creare il MOC separato ? Qualche idea su non eliminare l'intero database, ma usando sqlite per eliminare tutte le righe da una tabella specifica? - David

Si utilizza un MOC separato in modo che l'interfaccia utente non sia bloccata mentre è in corso l'operazione di cancellazione lunga. Si noti che quando avviene il commit effettivo sul database, solo un thread può accedere al database, quindi qualsiasi altro accesso (come il recupero) bloccherà eventuali aggiornamenti. Questo è un altro motivo per interrompere l'operazione di eliminazione di grandi dimensioni in blocchi. Piccole parti di lavoro forniranno una possibilità per altri MOC (s) di accedere al negozio senza dover attendere il completamento dell'intera operazione.

Se ciò causa problemi, è anche possibile implementare le code di priorità (tramite dispatch_set_target_queue), ma questo esula dallo scopo di questa domanda.

Per quanto riguarda l'utilizzo di comandi sqlite nel database Core Data, Apple ha ripetutamente affermato che questa è una pessima idea e non si dovrebbero eseguire comandi SQL diretti su un file di database Core Data.


Infine, fammi notare questo. Nella mia esperienza, ho scoperto che quando ho un serio problema di prestazioni, di solito è il risultato di una progettazione scadente o di un'implementazione impropria. Rivedi il tuo problema e vedi se riesci a riprogettare il tuo sistema in qualche modo per meglio adattarlo a questo caso d'uso.

Se è necessario inviare tutti i dati, è possibile interrogare il database in un thread in background e filtrare i nuovi dati in modo da suddividere i dati in tre set: oggetti che devono essere modificati, oggetti che devono essere eliminati e oggetti che devono essere inserito

In questo modo, si modifica solo il database in cui deve essere modificato.

Se i dati sono quasi nuovi ogni volta, prendere in considerazione la possibilità di ristrutturare il database in cui tali entità dispongono di un proprio database (presumo che il database contenga già più entità). In questo modo puoi semplicemente cancellare il file e ricominciare con un nuovo database. È veloce Ora, reinserire diverse migliaia di oggetti non sarà veloce.

È necessario gestire le relazioni manualmente, tra negozi. Non è difficile, ma non è automatico come le relazioni all'interno dello stesso negozio.

Se l'ho fatto, vorrei prima creare il nuovo database, quindi demolire quello esistente, sostituirlo con quello nuovo e quindi eliminare quello vecchio.

Se si sta manipolando il database solo tramite questo meccanismo batch e non è necessaria la gestione del grafo degli oggetti, è possibile prendere in considerazione l'utilizzo di sqlite anziché di Core Data.

+0

Grazie. Un sacco di buoni punti. Tutto bene spiegato tranne, perché creare il MOC separato? Qualche idea su non eliminare l'intero database, ma usando sqlite per eliminare tutte le righe da una tabella specifica? – David

+0

Buona risposta; Vorrei solo aggiungere che il modo in cui le entità hanno "il proprio database" è quello di utilizzare un NSPersistentStore separato per quelle entità, e quindi rimuovere solo quel negozio dal disco. È sufficiente una "configurazione" di NSPersistentStore per garantire che le entità di un certo tipo entrino in quel particolare negozio. –

+0

@JesseRusak Ottimo punto. Alcune persone non sanno che puoi avere più negozi. –

-1

Sì, è ragionevole eliminare lo store persistente e iniziare da zero. Questo succede abbastanza velocemente. Quello che puoi fare è rimuovere l'archivio persistente (con l'URL del negozio persistente) dal coordinatore dell'archivio permanente, quindi utilizzare l'url dell'archivio permanente per eliminare il file del database dalla cartella della directory. L'ho fatto usando removeItemAtURL di NSFileManager.

Edit: una cosa da considerare: assicurarsi di disattivare/rilasciare l'istanza NSManagedObjectContext corrente, e di fermare qualsiasi altro thread che potrebbe fare qualcosa con un NSManagedObjectContext che sta usando lo stesso archivio permanente. L'applicazione si arresta in modo anomalo se un contesto tenta di accedere all'archivio permanente.

1

In realtà l'unica opzione è rimuoverli individualmente. Faccio questo metodo con una tonnellata di oggetti ed è piuttosto veloce. Ecco un modo in cui qualcuno lo fa caricando solo l'ID oggetto gestito in modo da evitare sovraccarichi non necessari e renderlo più veloce.

Core Data: Quickest way to delete all instances of an entity

6

iOS 9 e versioni successive

Usa NSBatchDeleteRequest. Ho provato questo nel simulatore su un'entità Core Data con oltre 400.000 istanze e l'eliminazione era quasi istantanea.

// fetch all items in entity and request to delete them 
let fetchRequest = NSFetchRequest(entityName: "MyEntity") 
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) 

// delegate objects 
let myManagedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext 
let myPersistentStoreCoordinator = (UIApplication.sharedApplication().delegate as! AppDelegate).persistentStoreCoordinator 

// perform the delete 
do { 
    try myPersistentStoreCoordinator.executeRequest(deleteRequest, withContext: myManagedObjectContext) 
} catch let error as NSError { 
    print(error) 
} 

noti che the answer che @Bot legata e che @JodyHagins menzionati è stato aggiornato a questo metodo.

Problemi correlati