Obsługa błędów „produkcyjnych” danych podstawowych iPhone'a

84

W przykładowym kodzie dostarczonym przez Apple widziałem odniesienia do sposobu obsługi błędów podstawowych danych. To znaczy:

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

Ale nigdy nie ma przykładów, jak należy to wdrożyć.

Czy ktoś ma (lub może wskazać mi kierunek) jakiś rzeczywisty kod „produkcyjny”, który ilustruje powyższą metodę.

Z góry dzięki, Matt

Kołysać
źródło
7
+1 to doskonałe pytanie.
Dave DeLong,

Odpowiedzi:

32

Nikt nie pokaże Ci kodu produkcyjnego, ponieważ zależy on w 100% od aplikacji i miejsca wystąpienia błędu.

Osobiście umieściłem tam stwierdzenie assert, ponieważ 99,9% przypadków ten błąd będzie występował podczas programowania, a kiedy go tam naprawisz, jest bardzo mało prawdopodobne, że zobaczysz go w produkcji.

Po asercie przedstawię alert użytkownikowi, poinformuję go, że wystąpił nieodwracalny błąd i że aplikacja zostanie zamknięta. Możesz również umieścić tam notkę z prośbą o skontaktowanie się z programistą, aby mieć nadzieję, że będziesz mógł to śledzić.

Potem zostawiłbym abort () w tym miejscu, ponieważ spowoduje to "awarię" aplikacji i wygeneruje ślad stosu, którego, miejmy nadzieję, możesz później użyć do wyśledzenia problemu.

Marcus S. Zarra
źródło
Marcus - Chociaż potwierdzenia są w porządku, jeśli rozmawiasz z lokalną bazą danych sqlite lub plikiem XML, potrzebujesz bardziej niezawodnego mechanizmu obsługi błędów, jeśli Twój stały magazyn jest oparty na chmurze.
dar512
4
Jeśli Twój stały magazyn danych podstawowych systemu iOS jest oparty na chmurze, masz większe problemy.
Marcus S. Zarra
3
Nie zgadzam się z Apple w kilku kwestiach. To jest różnica między sytuacją dydaktyczną (Apple) a okopami (ja). Z sytuacji akademickiej tak, powinieneś usunąć aborty. W rzeczywistości przydają się do wychwytywania sytuacji, których nigdy nie wyobrażałeś sobie jako możliwych. Twórcy dokumentacji Apple lubią udawać, że każda sytuacja jest odpowiedzialna. 99,999% z nich tak. Co robisz dla naprawdę nieoczekiwanego? Zawalam się i generuję dziennik, aby dowiedzieć się, co się stało. Po to jest aborcja.
Marcus S. Zarra
1
@cschuff, żadne z nich nie ma wpływu na -save:wywołanie podstawowych danych . Wszystkie te warunki mają miejsce na długo przed tym, zanim Twój kod osiągnie ten punkt.
Marcus S. Zarra
3
Jest to oczekiwany błąd, który można wychwycić i poprawić przed zapisem. Możesz zapytać Core Data, czy dane są prawidłowe i poprawić je. Dodatkowo możesz to sprawdzić w momencie użycia, aby upewnić się, że wszystkie prawidłowe pola są obecne. Jest to błąd poziomu programisty, który można obsłużyć na długo przed -save:wywołaniem.
Marcus S. Zarra
32

Jest to jedna z metod, które wymyśliłem, aby obsłużyć i wyświetlić błędy walidacji na iPhonie. Ale Marcus ma rację: prawdopodobnie chciałbyś poprawić komunikaty, aby były bardziej przyjazne dla użytkownika. Ale to przynajmniej daje punkt wyjścia, aby zobaczyć, które pole nie zostało zweryfikowane i dlaczego.

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

Cieszyć się.

Johannes Fahrenkrug
źródło
3
Z pewnością nie widzę nic złego w tym kodzie. Wygląda solidnie. Osobiście wolę radzić sobie z błędami Core Data z asercją. Jeszcze nie widziałem, żeby ktoś trafił do produkcji, więc zawsze uważałem je za błędy rozwojowe, a nie potencjalne błędy produkcyjne. Chociaż to z pewnością inny poziom ochrony :)
Marcus S. Zarra
2
Marcus, o asercjach: Jaka jest Twoja opinia na temat zachowania kodu DRY w zakresie walidacji? Moim zdaniem bardzo pożądane jest zdefiniowanie kryteriów walidacji tylko raz, w modelu (do którego należy): To pole nie może być puste, to pole musi mieć co najmniej 5 znaków długości i to pole musi pasować do tego wyrażenia regularnego . To powinny być wszystkie informacje potrzebne do wyświetlenia odpowiedniej wiadomości użytkownikowi. W jakiś sposób nie podoba mi się ponowne sprawdzanie kodu przed zapisaniem MOC. Co myślisz?
Johannes Fahrenkrug,
2
Nigdy nie widziałem tego komentarza, ponieważ nie było go w mojej odpowiedzi. Nawet jeśli umieścisz walidację w modelu, nadal musisz sprawdzić, czy obiekt przeszedł walidację i przedstawić to użytkownikowi. W zależności od projektu, który może znajdować się na poziomie pola (to hasło jest złe itp.) Lub w punkcie zapisu. Wybór projektanta. Nie chciałbym, aby ta część aplikacji była ogólna.
Marcus S. Zarra
1
@ MarcusS.Zarra Wydaje mi się, że nigdy tego nie dostałeś, ponieważ nie poprawiłem @ -wzmianki o tobie :) Myślę, że w pełni się zgadzamy: chciałbym, aby walidacja- informacja była w modelu, ale decyzja, kiedy uruchomić walidację sposób obsługi i prezentacji wyniku walidacji nie powinien być ogólny i powinien być obsługiwany w odpowiednich miejscach w kodzie aplikacji.
Johannes Fahrenkrug
Kod wygląda świetnie. Moje jedyne pytanie brzmi: czy po wyświetleniu alertu lub zarejestrowaniu analizy powinienem cofnąć kontekst danych podstawowych, czy przerwać aplikację? W przeciwnym razie myślę, że niezapisane zmiany będą nadal powodować ten sam problem, gdy spróbujesz ponownie zapisać.
Jake
6

Dziwię się, że nikt tutaj tak naprawdę nie obsługuje błędu tak, jak powinien. Jeśli spojrzysz na dokumentację, zobaczysz.

Typowe przyczyny błędu to: * W urządzeniu brakuje miejsca. * Magazyn trwały jest niedostępny ze względu na uprawnienia lub ochronę danych, gdy urządzenie jest zablokowane. * Nie można przenieść sklepu do aktualnej wersji modelu. * Katalog nadrzędny nie istnieje, nie można go utworzyć lub nie zezwala na zapis.

Jeśli więc znajdę błąd podczas konfigurowania podstawowego stosu danych, zamieniam rootViewController UIWindow i pokazuję interfejs użytkownika, który wyraźnie informuje użytkownika, że ​​jego urządzenie może być pełne lub ustawienia zabezpieczeń są zbyt wysokie, aby ta aplikacja działała. Daję im również przycisk „spróbuj ponownie”, aby mogli spróbować rozwiązać problem przed ponowną próbą wykorzystania podstawowego stosu danych.

Na przykład użytkownik może zwolnić trochę miejsca na dysku, wrócić do mojej aplikacji i nacisnąć przycisk spróbuj ponownie.

Potwierdza? Naprawdę? Za dużo programistów w pokoju!

Jestem również zaskoczony liczbą samouczków online, w których nie wspomina się, jak operacja zapisywania może się nie powieść z tych powodów. Musisz więc upewnić się, że każde zdarzenie zapisu WSZĘDZIE w aplikacji może się nie powieść, ponieważ urządzenie TYLKO W TEJ MINUTIE zapełniło się zapisywaniem zapisywania aplikacji.


źródło
To pytanie dotyczy zapisywania w stosie danych podstawowych, a nie konfigurowania stosu danych podstawowych. Ale zgadzam się, że jego tytuł może wprowadzać w błąd i być może powinien zostać zmodyfikowany.
valeCocoa
Nie zgadzam się z @valeCocoa. Post wyraźnie dotyczy tego, jak radzić sobie z zapisywaniem błędów w produkcji. Spójrz jeszcze raz.
@roddanash, co powiedziałem… WtH! :) Spójrz jeszcze raz na swoją odpowiedź.
valeCocoa
Jesteś szalony bracie
wklejasz część dokumentacji pod kątem błędów, które mogą wystąpić podczas tworzenia instancji magazynu trwałego na pytanie dotyczące błędów występujących podczas zapisywania kontekstu, a ja jestem szalony? Ok…
valeCocoa
5

Uważam, że ta wspólna funkcja zapisywania jest znacznie lepszym rozwiązaniem:

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

Za każdym razem, gdy zapis nie powiedzie się, spowoduje to wycofanie NSManagedObjectContext, co oznacza, że ​​zresetuje wszystkie zmiany, które zostały wykonane w kontekście od ostatniego zapisu . Musisz więc uważać, aby zawsze utrwalać zmiany przy użyciu powyższej funkcji zapisywania tak wcześnie i regularnie, jak to możliwe, ponieważ w przeciwnym razie możesz łatwo utracić dane.

W przypadku wstawiania danych może to być luźniejszy wariant, umożliwiający życie innym zmianom:

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

Uwaga: do logowania tutaj używam CocoaLumberjack.

Każdy komentarz dotyczący tego, jak to poprawić, jest bardziej niż mile widziany!

BR Chris

mankiet
źródło
Dostaję dziwne zachowanie, gdy próbuję użyć wycofania, aby to osiągnąć: stackoverflow.com/questions/34426719/ ...
malhal
Zamiast tego
używam
2

Stworzyłem szybką wersję przydatnej odpowiedzi @JohannesFahrenkrug, która może być przydatna:

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
źródło