Manejo de errores de "producción" de datos básicos de iPhone

84

He visto en el código de ejemplo proporcionado por Apple referencias sobre cómo debe manejar los errores de Core Data. Es decir:

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

Pero nunca ejemplos de cómo debe implementarlo.

¿Alguien tiene (o me puede señalar en la dirección de) algún código de "producción" real que ilustre el método anterior?

Gracias de antemano, Matt

Influencia
fuente
7
+1 esta es una excelente pregunta.
Dave DeLong

Respuestas:

32

Nadie te va a mostrar el código de producción porque depende al 100% de tu aplicación y de dónde ocurre el error.

Personalmente, puse una declaración de aserción allí porque el 99,9% de las veces este error ocurrirá en el desarrollo y cuando lo arregle allí es muy poco probable que lo vea en producción.

Después de la afirmación, presentaría una alerta al usuario, le informaría que ocurrió un error irrecuperable y que la aplicación se cerrará. También puede poner una propaganda allí pidiéndoles que se comuniquen con el desarrollador para que, con suerte, pueda rastrear esto.

Después de eso, dejaría el abort () allí, ya que "bloqueará" la aplicación y generará un seguimiento de pila que, con suerte, podrá usar más adelante para rastrear el problema.

Marcus S. Zarra
fuente
Marcus: si bien las afirmaciones están bien si está hablando con una base de datos sqlite local o un archivo XML, necesita un mecanismo de manejo de errores más sólido si su almacén persistente está basado en la nube.
dar512
4
Si su almacén persistente de iOS Core Data está basado en la nube, tiene problemas mayores.
Marcus S. Zarra
3
No estoy de acuerdo con Apple en varios temas. Es la diferencia entre una situación docente (Apple) y en las trincheras (yo). Desde una situación académica, sí debes eliminar los abortos. En realidad, son útiles para captar situaciones que nunca imaginaste posibles. A los redactores de documentación de Apple les gusta pretender que cada situación es responsable. El 99,999% de ellos lo son. ¿Qué haces por lo verdaderamente inesperado? Me bloqueo y genero un registro para poder averiguar qué sucedió. Para eso es abortar.
Marcus S. Zarra
1
@cschuff, ninguno de ellos afecta una -save:llamada de datos centrales . Todas esas condiciones ocurren mucho antes de que su código llegue a este punto.
Marcus S. Zarra
3
Ese es un error anticipado que puede detectarse y corregirse antes de guardar. Puede preguntar a Core Data si los datos son válidos y corregirlos. Además, puede probarlo en el momento del consumo para asegurarse de que todos los campos válidos estén presentes. Ese es un error de nivel de desarrollador que se puede manejar mucho antes de que -save:se llame.
Marcus S. Zarra
32

Este es un método genérico que se me ocurrió para manejar y mostrar errores de validación en el iPhone. Pero Marcus tiene razón: probablemente querrá modificar los mensajes para que sean más fáciles de usar. Pero esto al menos le da un punto de partida para ver qué campo no se validó y por qué.

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

Disfrutar.

Johannes Fahrenkrug
fuente
3
Ciertamente no puedo ver nada malo en este código. Parece sólido. Personalmente, prefiero manejar los errores de Core Data con una afirmación. Todavía tengo que ver uno que llegue a producción, por lo que siempre los he considerado errores de desarrollo en lugar de posibles errores de producción. Aunque este es ciertamente otro nivel de protección :)
Marcus S. Zarra
2
Marcus, sobre las afirmaciones: ¿Cuál es su opinión sobre mantener el código SECO en términos de validaciones? En mi opinión, es muy deseable definir sus criterios de validación solo una vez, en el modelo (donde pertenece): este campo no puede estar vacío, ese campo debe tener al menos 5 caracteres de longitud y ese campo debe coincidir con esta expresión regular . Esa debería ser toda la información necesaria para mostrar un mensaje apropiado al usuario. De alguna manera, no me sienta bien hacer esas comprobaciones nuevamente en el código antes de guardar el MOC. ¿Qué piensas?
Johannes Fahrenkrug
2
Nunca vi este comentario ya que no estaba en mi respuesta. Incluso cuando pones la validación en el modelo, necesitas verificar si el objeto pasó la validación y presentárselo al usuario. Dependiendo del diseño que podría estar a nivel de campo (esta contraseña es mala, etc.) o en el punto de guardado. Elección del diseñador. No convertiría esa parte de la aplicación en genérica.
Marcus S. Zarra
1
@ MarcusS.Zarra Supongo que nunca lo obtuviste porque no lo @-mencioné correctamente :) Creo que estamos completamente de acuerdo: me gustaría que la información de validación esté en el modelo, pero la decisión de cuándo activar la validación y cómo manejar y presentar el resultado de la validación no debe ser genérico y debe manejarse en los lugares apropiados en el código de la aplicación.
Johannes Fahrenkrug
El código se ve muy bien. Mi única pregunta es, después de mostrar la alerta o registrar el análisis, ¿debería revertir el contexto de datos básicos o cancelar la aplicación? De lo contrario, supongo que los cambios no guardados seguirán causando el mismo problema cuando intente guardar nuevamente.
Jake
6

Me sorprende que nadie aquí esté manejando el error de la forma en que debe manejarse. Si miras la documentación, verás.

Las razones típicas de un error aquí incluyen: * El dispositivo no tiene espacio. * No se puede acceder al almacén persistente, debido a permisos o protección de datos cuando el dispositivo está bloqueado. * La tienda no se pudo migrar a la versión del modelo actual. * El directorio principal no existe, no se puede crear o no permite la escritura.

Entonces, si encuentro un error al configurar la pila de datos central, cambio el rootViewController de UIWindow y muestro una interfaz de usuario que le dice claramente al usuario que su dispositivo podría estar lleno o que sus configuraciones de seguridad son demasiado altas para que esta aplicación funcione. También les doy un botón "intentar de nuevo", para que puedan intentar solucionar el problema antes de que se vuelva a intentar la pila de datos centrales.

Por ejemplo, el usuario podría liberar espacio de almacenamiento, regresar a mi aplicación y presionar el botón intentar nuevamente.

Afirma? De Verdad? ¡Demasiados desarrolladores en la sala!

También me sorprende la cantidad de tutoriales en línea que no mencionan cómo una operación de guardado podría fallar también por estas razones. Por lo tanto, deberá asegurarse de que cualquier evento guardado en CUALQUIER LUGAR de su aplicación pueda fallar porque el dispositivo SOLO ESTE MINUTO se llenó con sus aplicaciones guardando guardado guardado.


fuente
Esta pregunta trata sobre el ahorro en la pila de datos centrales, no se trata de configurar la pila de datos centrales. Pero estoy de acuerdo en que su título podría ser engañoso y tal vez debería modificarse.
valeCocoa
No estoy de acuerdo @valeCocoa. La publicación trata claramente sobre cómo manejar los errores de guardado en producción. Eche otro vistazo.
@roddanash que es lo que dije… ¡WtH! :) Eche otro vistazo a su respuesta.
valeCocoa
Estás loco hermano
pegas parte de la documentación de los errores que pueden ocurrir al crear una instancia del almacén persistente en una pregunta relacionada con los errores que ocurren al guardar el contexto, ¿y yo soy el loco? Ok…
valeCocoa
5

Encontré esta función de guardado común una solución mucho mejor:

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

Siempre que un guardado falle, esto revertirá su NSManagedObjectContext, lo que significa que restablecerá todos los cambios que se han realizado en el contexto desde el último guardado . Por lo tanto, debe tener cuidado para mantener siempre los cambios utilizando la función de guardado anterior lo antes posible y con regularidad, ya que de lo contrario podría perder datos fácilmente.

Para insertar datos, esta podría ser una variante más flexible que permita que otros cambios continúen:

- (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: Estoy usando CocoaLumberjack para iniciar sesión aquí.

¡Cualquier comentario sobre cómo mejorar esto es más que bienvenido!

BR Chris

cschuff
fuente
Obtengo un
Estoy usando deshacer en su lugar ahora
malhal
2

Hice una versión rápida de la útil respuesta de @JohannesFahrenkrug que puede ser útil:

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
fuente