Gestione degli errori di "produzione" dei dati di base di iPhone


85

Ho visto nel codice di esempio fornito da Apple riferimenti a come dovresti gestire gli errori di Core Data. Cioè:

NSError *error = nil;
if (![context save:&error]) {
/*
 Replace this implementation with code to handle the error appropriately.

 abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
 */
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}

Ma mai nessun esempio di come dovresti implementarlo.

Qualcuno ha (o può indicarmi nella direzione di) un codice di "produzione" effettivo che illustri il metodo di cui sopra.

Grazie in anticipo, Matt


8
+1 questa è un'ottima domanda.
Dave DeLong

Risposte:


33

Nessuno ti mostrerà il codice di produzione perché dipende al 100% dalla tua applicazione e da dove si verifica l'errore.

Personalmente, ho inserito una dichiarazione di asserzione perché il 99,9% delle volte questo errore si verificherà in fase di sviluppo e quando lo risolverai è altamente improbabile che lo vedrai in produzione.

Dopo l'asserzione presenterei un avviso all'utente, informalo che si è verificato un errore irreversibile e che l'applicazione sta per uscire. Puoi anche inserire un blurb chiedendo loro di contattare lo sviluppatore in modo che tu possa, si spera, tenere traccia di questo fatto.

Dopodiché lascerei l'abort () lì dentro perché "manderà in crash" l'app e genererà una traccia dello stack che si spera tu possa usare in seguito per rintracciare il problema.


Marcus - Anche se le affermazioni vanno bene se stai parlando con un database sqlite locale o un file XML, hai bisogno di un meccanismo di gestione degli errori più robusto se il tuo archivio persistente è basato su cloud.
dar512

4
Se il tuo archivio persistente di iOS Core Data è basato su cloud, hai problemi maggiori.
Marcus S. Zarra

3
Non sono d'accordo con Apple su una serie di argomenti. È la differenza tra una situazione di insegnamento (Apple) e in trincea (io). Da una situazione accademica, sì, dovresti rimuovere gli aborti. In realtà sono utili per cogliere situazioni che non avresti mai immaginato possibili. Agli autori di documentazione Apple piace fingere che ogni situazione sia responsabile. Il 99,999% di loro lo è. Cosa fai per il vero inaspettato? Mi blocca e genera un registro in modo da poter scoprire cosa è successo. Ecco a cosa serve l'aborto.
Marcus S. Zarra

1
@cschuff, nessuno di questi influisce su una -save:chiamata dati di base . Tutte queste condizioni si verificano molto prima che il codice raggiunga questo punto.
Marcus S. Zarra

3
Questo è un errore anticipato che può essere rilevato e corretto prima del salvataggio. Puoi chiedere a Core Data se i dati sono validi e correggerli. Inoltre puoi testarlo al momento del consumo per assicurarti che tutti i campi validi siano presenti. Questo è un errore a livello di sviluppatore che può essere gestito molto prima che -save:venga chiamato.
Marcus S. Zarra

32

Questo è un metodo generico che ho escogitato per gestire e visualizzare gli errori di convalida su iPhone. Ma Marcus ha ragione: probabilmente vorresti modificare i messaggi per renderli più user friendly. Ma questo almeno ti dà un punto di partenza per vedere quale campo non è stato convalidato e perché.

- (void)displayValidationError:(NSError *)anError {
    if (anError && [[anError domain] isEqualToString:@"NSCocoaErrorDomain"]) {
        NSArray *errors = nil;

        // multiple errors?
        if ([anError code] == NSValidationMultipleErrorsError) {
            errors = [[anError userInfo] objectForKey:NSDetailedErrorsKey];
        } else {
            errors = [NSArray arrayWithObject:anError];
        }

        if (errors && [errors count] > 0) {
            NSString *messages = @"Reason(s):\n";

            for (NSError * error in errors) {
                NSString *entityName = [[[[error userInfo] objectForKey:@"NSValidationErrorObject"] entity] name];
                NSString *attributeName = [[error userInfo] objectForKey:@"NSValidationErrorKey"];
                NSString *msg;
                switch ([error code]) {
                    case NSManagedObjectValidationError:
                        msg = @"Generic validation error.";
                        break;
                    case NSValidationMissingMandatoryPropertyError:
                        msg = [NSString stringWithFormat:@"The attribute '%@' mustn't be empty.", attributeName];
                        break;
                    case NSValidationRelationshipLacksMinimumCountError:  
                        msg = [NSString stringWithFormat:@"The relationship '%@' doesn't have enough entries.", attributeName];
                        break;
                    case NSValidationRelationshipExceedsMaximumCountError:
                        msg = [NSString stringWithFormat:@"The relationship '%@' has too many entries.", attributeName];
                        break;
                    case NSValidationRelationshipDeniedDeleteError:
                        msg = [NSString stringWithFormat:@"To delete, the relationship '%@' must be empty.", attributeName];
                        break;
                    case NSValidationNumberTooLargeError:                 
                        msg = [NSString stringWithFormat:@"The number of the attribute '%@' is too large.", attributeName];
                        break;
                    case NSValidationNumberTooSmallError:                 
                        msg = [NSString stringWithFormat:@"The number of the attribute '%@' is too small.", attributeName];
                        break;
                    case NSValidationDateTooLateError:                    
                        msg = [NSString stringWithFormat:@"The date of the attribute '%@' is too late.", attributeName];
                        break;
                    case NSValidationDateTooSoonError:                    
                        msg = [NSString stringWithFormat:@"The date of the attribute '%@' is too soon.", attributeName];
                        break;
                    case NSValidationInvalidDateError:                    
                        msg = [NSString stringWithFormat:@"The date of the attribute '%@' is invalid.", attributeName];
                        break;
                    case NSValidationStringTooLongError:      
                        msg = [NSString stringWithFormat:@"The text of the attribute '%@' is too long.", attributeName];
                        break;
                    case NSValidationStringTooShortError:                 
                        msg = [NSString stringWithFormat:@"The text of the attribute '%@' is too short.", attributeName];
                        break;
                    case NSValidationStringPatternMatchingError:          
                        msg = [NSString stringWithFormat:@"The text of the attribute '%@' doesn't match the required pattern.", attributeName];
                        break;
                    default:
                        msg = [NSString stringWithFormat:@"Unknown error (code %i).", [error code]];
                        break;
                }

                messages = [messages stringByAppendingFormat:@"%@%@%@\n", (entityName?:@""),(entityName?@": ":@""),msg];
            }
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Validation Error" 
                                                            message:messages
                                                           delegate:nil 
                                                  cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
            [alert show];
            [alert release];
        }
    }
}

Godere.


3
Certamente non riesco a vedere nulla di sbagliato in questo codice. Sembra solido. Personalmente preferisco gestire gli errori di Core Data con un'asserzione. Devo ancora vederne uno arrivare alla produzione, quindi li ho sempre considerati errori di sviluppo piuttosto che potenziali errori di produzione. Anche se questo è certamente un altro livello di protezione :)
Marcus S. Zarra

2
Marcus, a proposito delle affermazioni: qual è la tua opinione sul mantenere il codice DRY in termini di convalide? A mio parere è molto desiderabile definire i criteri di convalida solo una volta, nel modello (a cui appartiene): questo campo non può essere vuoto, quel campo deve essere lungo almeno 5 caratteri e quel campo deve corrispondere a questa regex . Questo dovrebbe essere tutte le informazioni necessarie per visualizzare un msg appropriata per l'utente. In qualche modo non mi va bene fare di nuovo quei controlli nel codice prima di salvare il MOC. Cosa pensi?
Johannes Fahrenkrug

2
Non ho mai visto questo commento poiché non era sulla mia risposta. Anche quando inserisci la convalida nel modello, devi comunque controllare se l'oggetto ha superato la convalida e presentarlo all'utente. A seconda del design che potrebbe essere a livello di campo (questa password è errata, ecc.) O al punto di salvataggio. Scelta del designer. Non renderei quella parte dell'app generica.
Marcus S. Zarra

1
@ MarcusS.Zarra Immagino che tu non l'abbia mai capito perché non ti ho fatto @ -menzionare correttamente :) Penso che siamo pienamente d'accordo: vorrei che le informazioni di convalida fossero nel modello, ma la decisione su quando attivare la convalida e come gestire e presentare il risultato della convalida non dovrebbe essere generico e dovrebbe essere gestito nelle posizioni appropriate del codice dell'applicazione.
Johannes Fahrenkrug

Il codice sembra fantastico. La mia unica domanda è, dopo aver mostrato l'avviso o aver registrato l'analisi, devo eseguire il rollback del contesto dei dati di base o interrompere l'app? Altrimenti, immagino che le modifiche non salvate continueranno a causare lo stesso problema quando proverai a salvare di nuovo.
Jake

6

Sono sorpreso che nessuno qui stia effettivamente gestendo l'errore nel modo in cui dovrebbe essere gestito. Se guardi la documentazione, vedrai.

I motivi tipici per un errore qui includono: * Il dispositivo ha esaurito lo spazio. * L'archivio persistente non è accessibile, a causa delle autorizzazioni o della protezione dei dati quando il dispositivo è bloccato. * Non è stato possibile migrare il negozio alla versione del modello corrente. * La directory principale non esiste, non può essere creata o impedisce la scrittura.

Quindi, se trovo un errore durante la configurazione dello stack di dati di base, scambio il rootViewController di UIWindow e mostro l'interfaccia utente che dice chiaramente all'utente che il suo dispositivo potrebbe essere pieno o che le sue impostazioni di sicurezza sono troppo alte per far funzionare questa app. Fornisco loro anche un pulsante "riprova", in modo che possano tentare di risolvere il problema prima che venga riprovato lo stack di dati di base.

Ad esempio, l'utente potrebbe liberare spazio di archiviazione, tornare alla mia app e premere il pulsante Riprova.

Afferma? Veramente? Troppi sviluppatori nella stanza!

Sono anche sorpreso dal numero di tutorial online che non menzionano come un'operazione di salvataggio potrebbe fallire anche per questi motivi. Quindi dovrai assicurarti che qualsiasi evento di salvataggio OVUNQUE nella tua app possa fallire perché il dispositivo APPENA QUESTO MINUTO si è riempito con le tue app salvando il salvataggio.


Questa domanda riguarda il salvataggio nello stack Core Data, non si tratta di impostare il Core Data Stack. Ma sono d'accordo che il suo titolo potrebbe essere fuorviante e forse dovrebbe essere modificato.
valeCocoa

Non sono d'accordo @valeCocoa. Il post spiega chiaramente come gestire gli errori di salvataggio nella produzione. Dai un'altra occhiata.

@roddanash che è quello che ho detto ... WtH! :) Dai un'altra occhiata alla tua risposta.
valeCocoa

Sei pazzo fratello

incolli parte della documentazione per gli errori che possono verificarsi durante l'istanza dell'archivio persistente su una domanda riguardante gli errori che si verificano durante il salvataggio del contesto, e io sono il matto? Ok ...
valeCocoa

5

Ho trovato questa funzione di salvataggio comune una soluzione molto migliore:

- (BOOL)saveContext {
    NSError *error;
    if (![self.managedObjectContext save:&error]) {
        DDLogError(@"[%@::%@] Whoops, couldn't save managed object context due to errors. Rolling back. Error: %@\n\n", NSStringFromClass([self class]), NSStringFromSelector(_cmd), error);
        [self.managedObjectContext rollback];
        return NO;
    }
    return YES;
}

Ogni volta che un salvataggio fallisce, questo ripristinerà il tuo NSManagedObjectContext, il che significa che ripristinerà tutte le modifiche che sono state eseguite nel contesto dall'ultimo salvataggio . Quindi devi stare attento a mantenere sempre le modifiche utilizzando la funzione di salvataggio di cui sopra il più presto e regolarmente possibile poiché altrimenti potresti facilmente perdere i dati.

Per l'inserimento dei dati questa potrebbe essere una variante più flessibile che consente ad altre modifiche di sopravvivere:

- (BOOL)saveContext {
    NSError *error;
    if (![self.managedObjectContext save:&error]) {
        DDLogError(@"[%@::%@] Whoops, couldn't save. Removing erroneous object from context. Error: %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), object.objectId, error);
        [self.managedObjectContext deleteObject:object];
        return NO;
    }
    return YES;
}

Nota: sto usando CocoaLumberjack per la registrazione qui.

Qualsiasi commento su come migliorarlo è più che benvenuto!

BR Chris


Sto diventando un comportamento strano quando provo ad usare rollback per raggiungere questo obiettivo: stackoverflow.com/questions/34426719/...
malhal

Ora sto usando Annulla invece
malhal

2

Ho creato una versione Swift dell'utile risposta di @JohannesFahrenkrug che può essere utile:

public func displayValidationError(anError:NSError?) -> String {
    if anError != nil && anError!.domain.compare("NSCocoaErrorDomain") == .OrderedSame {
        var messages:String = "Reason(s):\n"
        var errors = [AnyObject]()
        if (anError!.code == NSValidationMultipleErrorsError) {
            errors = anError!.userInfo[NSDetailedErrorsKey] as! [AnyObject]
        } else {
            errors = [AnyObject]()
            errors.append(anError!)
        }
        if (errors.count > 0) {
            for error in errors {
                if (error as? NSError)!.userInfo.keys.contains("conflictList") {
                    messages =  messages.stringByAppendingString("Generic merge conflict. see details : \(error)")
                }
                else
                {
                    let entityName = "\(((error as? NSError)!.userInfo["NSValidationErrorObject"] as! NSManagedObject).entity.name)"
                    let attributeName = "\((error as? NSError)!.userInfo["NSValidationErrorKey"])"
                    var msg = ""
                    switch (error.code) {
                    case NSManagedObjectValidationError:
                        msg = "Generic validation error.";
                        break;
                    case NSValidationMissingMandatoryPropertyError:
                        msg = String(format:"The attribute '%@' mustn't be empty.", attributeName)
                        break;
                    case NSValidationRelationshipLacksMinimumCountError:
                        msg = String(format:"The relationship '%@' doesn't have enough entries.", attributeName)
                        break;
                    case NSValidationRelationshipExceedsMaximumCountError:
                        msg = String(format:"The relationship '%@' has too many entries.", attributeName)
                        break;
                    case NSValidationRelationshipDeniedDeleteError:
                        msg = String(format:"To delete, the relationship '%@' must be empty.", attributeName)
                        break;
                    case NSValidationNumberTooLargeError:
                        msg = String(format:"The number of the attribute '%@' is too large.", attributeName)
                        break;
                    case NSValidationNumberTooSmallError:
                        msg = String(format:"The number of the attribute '%@' is too small.", attributeName)
                        break;
                    case NSValidationDateTooLateError:
                        msg = String(format:"The date of the attribute '%@' is too late.", attributeName)
                        break;
                    case NSValidationDateTooSoonError:
                        msg = String(format:"The date of the attribute '%@' is too soon.", attributeName)
                        break;
                    case NSValidationInvalidDateError:
                        msg = String(format:"The date of the attribute '%@' is invalid.", attributeName)
                        break;
                    case NSValidationStringTooLongError:
                        msg = String(format:"The text of the attribute '%@' is too long.", attributeName)
                        break;
                    case NSValidationStringTooShortError:
                        msg = String(format:"The text of the attribute '%@' is too short.", attributeName)
                        break;
                    case NSValidationStringPatternMatchingError:
                        msg = String(format:"The text of the attribute '%@' doesn't match the required pattern.", attributeName)
                        break;
                    default:
                        msg = String(format:"Unknown error (code %i).", error.code) as String
                        break;
                    }

                    messages = messages.stringByAppendingString("\(entityName).\(attributeName):\(msg)\n")
                }
            }
        }
        return messages
    }
    return "no error"
}`
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.