Traitement des erreurs de «production» des données de base de l'iPhone

84

J'ai vu dans l'exemple de code fourni par Apple des références à la façon dont vous devez gérer les erreurs Core Data. C'est à dire:

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();
}

Mais jamais d'exemples de la façon dont vous devriez l' implémenter.

Quelqu'un a-t-il (ou peut-il me diriger vers) un code de «production» qui illustre la méthode ci-dessus.

Merci d'avance, Matt

Balancement
la source
7
+1 c'est une excellente question.
Dave DeLong

Réponses:

32

Personne ne vous montrera le code de production car il dépend à 100% de votre application et de l'endroit où l'erreur se produit.

Personnellement, j'y ai mis une déclaration d'assert parce que 99,9% du temps, cette erreur se produira en développement et lorsque vous la corrigerez, il est très peu probable que vous la voyiez en production.

Après l'affirmation, je présenterais une alerte à l'utilisateur, lui ferais savoir qu'une erreur irrécupérable s'est produite et que l'application va se fermer. Vous pouvez également y mettre un texte de présentation leur demandant de contacter le développeur afin que vous puissiez, espérons-le, suivre cette opération.

Après cela, je laisserais abort () là-dedans car il "plantera" l'application et générera une trace de pile que vous pourrez, espérons-le, utiliser plus tard pour localiser le problème.

Marcus S. Zarra
la source
Marcus - Bien que les assertions conviennent si vous parlez à une base de données sqlite locale ou à un fichier XML, vous avez besoin d'un mécanisme de gestion des erreurs plus robuste si votre magasin persistant est basé sur le cloud.
dar512 le
4
Si votre magasin persistant iOS Core Data est basé sur le cloud, vous rencontrez des problèmes plus importants.
Marcus S.Zarra
3
Je ne suis pas d'accord avec Apple sur un certain nombre de sujets. C'est la différence entre une situation d'enseignement (Apple) et dans les tranchées (moi). D'une situation académique, oui, vous devez supprimer les avortements. En réalité, ils sont utiles pour saisir des situations que vous n'auriez jamais imaginées possibles. Les rédacteurs de documentation Apple aiment prétendre que chaque situation est responsable. 99,999% d'entre eux le sont. Que faites-vous pour le vraiment inattendu? Je plante et génère un journal pour savoir ce qui s'est passé. C'est à cela que sert l'abandon.
Marcus S.Zarra
1
@cschuff, aucun de ceux-ci n'a d'impact sur un -save:appel de données de base . Toutes ces conditions se produisent bien avant que votre code n'atteigne ce point.
Marcus
3
Il s'agit d'une erreur anticipée qui peut être détectée et corrigée avant la sauvegarde. Vous pouvez demander à Core Data si les données sont valides et les corriger. De plus, vous pouvez tester cela au moment de la consommation pour vous assurer que tous les champs valides sont présents. Il s'agit d'une erreur de niveau développeur qui peut être gérée bien avant l' -save:appel de.
Marcus S.Zarra
32

C'est une méthode générique que j'ai mise au point pour gérer et afficher les erreurs de validation sur l'iPhone. Mais Marcus a raison: vous voudrez probablement modifier les messages pour qu'ils soient plus conviviaux. Mais cela vous donne au moins un point de départ pour voir quel champ n'a pas validé et pourquoi.

- (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];
        }
    }
}

Prendre plaisir.

Johannes Fahrenkrug
la source
3
Je ne vois certainement rien de mal avec ce code. Ça a l'air solide. Personnellement, je préfère gérer les erreurs de Core Data avec une assertion. Je n'en ai pas encore vu arriver à la production, donc je les ai toujours considérés comme des erreurs de développement plutôt que des erreurs de production potentielles. Bien que ce soit certainement un autre niveau de protection :)
Marcus S.Zarra
2
Marcus, sur les assertions: Quelle est votre opinion sur le maintien du code DRY en termes de validations? À mon avis, il est très souhaitable de définir vos critères de validation une seule fois, dans le modèle (auquel il appartient): Ce champ ne peut pas être vide, ce champ doit comporter au moins 5 caractères et ce champ doit correspondre à cette expression régulière . Cela devrait être toutes les informations nécessaires pour afficher un msg approprié à l'utilisateur. Cela ne me convient pas d'une manière ou d'une autre de refaire ces vérifications dans le code avant d'enregistrer le MOC. Qu'est-ce que tu penses?
Johannes Fahrenkrug
2
Je n'ai jamais vu ce commentaire car ce n'était pas sur ma réponse. Même lorsque vous mettez la validation dans le modèle, vous devez toujours vérifier si l'objet a réussi la validation et le présenter à l'utilisateur. Selon la conception qui pourrait être au niveau du champ (ce mot de passe est incorrect, etc.) ou au point de sauvegarde. Le choix du designer. Je ne rendrais pas cette partie de l'application générique.
Marcus S.Zarra
1
@ MarcusS.Zarra Je suppose que vous ne l'avez obtenu parce que je ne l' ai pas correctement @ -mention vous :) Je pense que nous sommes d' accord pleinement: Je voudrais que le validation- informations comme dans le modèle, mais la décision quand déclencher la validation et comment gérer et présenter le résultat de la validation ne doit pas être générique et doit être traité aux endroits appropriés dans le code de l'application.
Johannes Fahrenkrug
Le code est superbe. Ma seule question est, après avoir affiché l'alerte ou enregistré l'analyse, dois-je restaurer le contexte Core Data ou abandonner l'application? Sinon, je suppose que les modifications non enregistrées continueront de causer le même problème lorsque vous essayez de sauvegarder à nouveau.
Jake
6

Je suis surpris que personne ici ne gère réellement l'erreur de la manière dont elle est censée être traitée. Si vous regardez la documentation, vous verrez.

Voici les raisons typiques d'une erreur: * Le périphérique manque d'espace. * Le magasin persistant n'est pas accessible, en raison des autorisations ou de la protection des données lorsque l'appareil est verrouillé. * Le magasin n'a pas pu être migré vers la version actuelle du modèle. * Le répertoire parent n'existe pas, ne peut pas être créé ou interdit l'écriture.

Donc, si je trouve une erreur lors de la configuration de la pile de données de base, j'échange le rootViewController de UIWindow et affiche l'interface utilisateur qui indique clairement à l'utilisateur que son appareil est peut-être plein ou que ses paramètres de sécurité sont trop élevés pour que cette application fonctionne. Je leur donne également un bouton «réessayer», afin qu'ils puissent tenter de résoudre le problème avant que la pile de données de base ne soit réessayée.

Par exemple, l'utilisateur peut libérer de l'espace de stockage, revenir à mon application et appuyer sur le bouton réessayer.

Affirme? Vraiment? Trop de développeurs dans la salle!

Je suis également surpris par le nombre de tutoriels en ligne qui ne mentionnent pas comment une opération de sauvegarde pourrait échouer pour ces raisons également. Vous devrez donc vous assurer que tout événement de sauvegarde N'IMPORTE O dans votre application puisse échouer car l'appareil JUST CETTE MINUTE est devenu plein avec votre sauvegarde de sauvegarde des applications.


la source
Cette question concerne la sauvegarde dans la pile de données de base, il ne s'agit pas de configurer la pile de données de base. Mais je conviens que son titre pourrait être trompeur et qu'il devrait peut-être être modifié.
valeCocoa
Je ne suis pas d'accord avec @valeCocoa. Le message explique clairement comment gérer les erreurs de sauvegarde en production. Jetez un autre coup d'œil.
@roddanash qui est ce que j'ai dit… WtH! :) Jetez un autre regard sur votre réponse.
valeCocoa le
Vous êtes fou bro
vous collez une partie de la documentation pour les erreurs qui peuvent survenir lors de l'instanciation du magasin persistant sur une question concernant les erreurs survenant lors de l'enregistrement du contexte, et je suis fou? Ok…
valeCocoa
5

J'ai trouvé cette fonction de sauvegarde commune une bien meilleure solution:

- (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;
}

Chaque fois qu'une sauvegarde échoue, cela annulera votre NSManagedObjectContext, ce qui signifie qu'il réinitialisera toutes les modifications qui ont été effectuées dans le contexte depuis la dernière sauvegarde . Vous devez donc faire attention à toujours conserver les modifications en utilisant la fonction de sauvegarde ci-dessus le plus tôt et le plus régulièrement possible, car vous pourriez facilement perdre des données autrement.

Pour insérer des données, il peut s'agir d'une variante plus lâche permettant à d'autres modifications de vivre:

- (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;
}

Remarque: j'utilise CocoaLumberjack pour me connecter ici.

Tout commentaire sur la façon d'améliorer cela est plus que bienvenu!

BR Chris

cschuff
la source
Je reçois un comportement étrange lorsque j'essaie d'utiliser la restauration pour y parvenir: stackoverflow.com/questions/34426719
...
J'utilise l'annulation à la place maintenant
malhal
2

J'ai fait une version Swift de la réponse utile de @JohannesFahrenkrug qui peut être 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"
}`
cdescours
la source