Статьи

Что такое основная ошибка данных?

Ошибки являются важным компонентом Базовых данных. Несмотря на то, что этот термин звучит зловеще, ошибки свойственны жизненному циклу записи базовых данных. В этом руководстве вы узнаете, что такое ошибки, как их устранять и как отлаживать проблемы, связанные с ошибками.

Базовые данные — это сложная тема, поэтому я предполагаю, что вы уже знакомы с разработкой Xcode и iOS. Несмотря на то, что я буду использовать язык программирования Swift для этого урока, все в этом уроке также применимо к Objective-C.

Основные данные очень хорошо, что он делает благодаря упорной работе команды Core Data Apple. Базовые данные высоко оптимизированы, чтобы сохранить низкий объем памяти без ущерба для производительности. Неисправность — это один из методов, которые Core Data использует, чтобы потреблять как можно меньше памяти.

Ошибка не уникальна для Базовых Данных. Подобный метод используется во многих других средах, таких как Ember и Ruby on Rails . Идея проста, загружать данные только тогда, когда это необходимо. Чтобы заставить сбои работать, Core Data делает что-то волшебное, создавая собственные подклассы во время компиляции, которые представляют неисправности. Давайте посмотрим, как это работает на примере.

Я создал простой пример приложения для работы. Загрузите проект Xcode с GitHub и откройте его в Xcode. Проект использует Swift 2.1, что означает, что вам нужен Xcode 7.1 или выше, чтобы удовлетворить компилятор.

Модель данных содержит две сущности, List и Item . Список может содержать ноль или более элементов, и элемент всегда связан со списком. Это классический пример отношения один ко многим.

Модель данных

Запустите приложение и заполните постоянное хранилище, базу данных SQLite, некоторыми данными, создав несколько списков и элементов. Выйдите из приложения, когда закончите.

Теперь у вас должно быть приложение с некоторыми данными. Давайте посмотрим, как сбои работают на практике, добавив несколько операторов печати. Откройте ListsViewController.swift и найдите prepareForSegue(_:sender:) . В этом методе мы выбираем список, выбранный пользователем в табличном представлении. prepareForSegue(_:sender:) операторы print в prepareForSegue(_:sender:) как показано в приведенной ниже реализации.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == SegueListViewController {
        guard let indexPath = tableView.indexPathForSelectedRow else { return }
         
        // Fetch List
        let list = self.fetchedResultsController.objectAtIndexPath(indexPath) as!
         
        print(«1: \(list)»)
         
        if let items = list.items {
            print(«2: \(items)»)
            print(«3: \(items.count)»)
            print(«4: \(items)»)
             
            if let item = items.anyObject() {
                print(«5: \(item)»)
                print(«6: \(item.name)»)
                print(«7: \(item)»)
            }
        }
         
        print(«8: \(list)»)
         
        // Fetch Destination View Controller
        let listViewController = segue.destinationViewController as!
         
        // Configure View Controller
        listViewController.list = list
    }
}

Запустите приложение и коснитесь одного из списков в контроллере представления списков. Этот пример интересен только в том случае, если вы коснетесь списка, с которым связан один или несколько элементов. Давайте рассмотрим вывод операторов print шаг за шагом.

1
2
3
4
1: <Faulting.List: 0x154e5d0b0> (entity: List; id: 0xd0000000001c0000 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/List/p7> ; data: {
    items = «<relationship fault: 0x154e3d5b0 ‘items’>»;
    name = «List 6»;
})

Первое, на что стоит обратить внимание — это имя класса Faulting.List . Это то, что мы ожидаем, так как модуль называется Faulting, а класс называется List . В Swift полное имя класса класса состоит из имени модуля и имени класса.

Вывод в консоли также показывает имя объекта, список и словарь данных. Атрибут имени установлен в Список 6, в то время как атрибут items помечен как ошибка связи . Это первый тип ошибок в базовых данных. Базовые данные понимают, что нет необходимости загружать отношения. Вместо этого это отмечает отношения как ошибку. Второе утверждение печати подтверждает это, как вы можете видеть ниже.

1
2
3
4
2: Relationship ‘items’ fault on managed object (0x154e5d0b0) <Faulting.List: 0x154e5d0b0> (entity: List; id: 0xd0000000001c0000 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/List/p7> ; data: {
    items = «<relationship fault: 0x154e3d5b0 ‘items’>»;
    name = «List 6»;
})

Третье утверждение печати, однако, более интересно. Он печатает количество элементов, связанных со списком.

1
3: 2

Базовые данные могут сделать это, только запустив ошибку связи. Он запрашивает постоянное хранилище для элементов, связанных со списком. Это, однако, только часть истории, как показано в четвертом заявлении.

01
02
03
04
05
06
07
08
09
10
4: Relationship ‘items’ on managed object (0x154e5d0b0) <Faulting.List: 0x154e5d0b0> (entity: List; id: 0xd0000000001c0000 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/List/p7> ; data: {
    items = (
        «0xd000000000540002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p21>»,
        «0xd000000000580002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p22>»
    );
    name = «List 6»;
}) with objects {(
    <Faulting.Item: 0x154e83d60> (entity: Item; id: 0xd000000000540002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p21> ; data: <fault>),
    <Faulting.Item: 0x154e55e50> (entity: Item; id: 0xd000000000580002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p22> ; data: <fault>)
)}

Мы больше не видим ошибки в отношениях, но мы все еще видим ошибку. О чем это? Базовые данные могут дать нам количество элементов в списке только путем устранения или устранения ошибки связи. Однако это не означает, что Core Data разрешает элементы отношений. Вывод в консоли подтверждает это.

Мы видим, что есть записи для элементов, включая идентификатор, который Core Data использует для внутренних целей. Словарь данных, однако, помечен как ошибка. Опять же, Core Data дает нам только то, что мы просим. К счастью, мельчайшие детали обрабатываются Core Data.

Давайте копнем немного глубже и выберем один из предметов из списка. Мы делаем это, вызывая anyObject() для объекта items . Мы печатаем полученный элемент в пятом операторе печати.

1
5: <Faulting.Item: 0x144d7f100> (entity: Item; id: 0xd000000000540002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p21> ; data: <fault>)

Вывод не должен вас удивлять. Вывод подтверждает, что мы имеем дело с сущностью Item . Неудивительно, что словарь данных по-прежнему помечен как ошибка. В шестом операторе print мы печатаем атрибут name элемента.

1
6: Item 0

Поскольку мы запрашиваем значение атрибута записи, Core Data запускает ошибку, чтобы дать нам это значение. Он выбирает данные для элемента и заполняет словарь данных. Седьмое печатное заявление подтверждает эти выводы.

1
2
3
4
7: <Faulting.Item: 0x144d7f100> (entity: Item; id: 0xd000000000540002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p21> ; data: {
    list = «0xd0000000001c0000 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/List/p7>»;
    name = «Item 0»;
})

Словарь данных содержит атрибут имени, а также отношение списка . Восьмой и последний оператор печати показывает, что ошибка отношения объекта list устранена.

1
2
3
4
5
6
7
8: <Faulting.List: 0x144e55da0> (entity: List; id: 0xd0000000001c0000 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/List/p7> ; data: {
    items = (
        «0xd000000000540002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p21>»,
        «0xd000000000580002 <x-coredata://804E0602-42B4-4EB5-A1D0-653848363595/Item/p22>»
    );
    name = «List 6»;
})

Я решил написать эту статью, чтобы объяснить проблему, с которой многие разработчики, использующие Core Data, сталкиваются в тот или иной момент, вызывая ошибку, которая не может быть решена. Проблема проста. Предположим, у вас есть список с несколькими элементами, и в какой-то момент пользователь удаляет список. Что происходит с элементами этого списка? Что произойдет, если Core Data попытается запустить отношение списка одного из элементов, принадлежащих этому списку? Давай выясним.

Давайте вернемся к проекту, который вы скачали с GitHub . Откройте Faulting.xcdatamodeld , модель данных проекта. Выберите отношение элементов объекта List и откройте инспектор модели данных справа. Нас интересует правило удаления , которое в настоящее время установлено в каскад . Это означает, что каждый элемент списка удаляется при удалении списка. Это имеет смысл, поскольку мы не хотим, чтобы оставленные элементы не были связаны со списком.

Удалить каскад правил

Выберите отношение списка объекта Item и откройте инспектор модели данных . Правило удаления этого отношения установлено в Nullify . Это означает, что пункт назначения отношения, объект списка, устанавливается равным нулю при удалении записи пункта назначения, элемента. Это правило удаления по умолчанию для отношений.

Удалить правило обнулить

Запустите приложение, создайте несколько списков и элементов и нажмите кнопку « Элементы» в левом верхнем углу, чтобы отобразить каждый элемент, который вы создали. Нажмите « Отмена» в левом верхнем углу, удалите один из списков и снова нажмите кнопку « Элементы» , чтобы увидеть, что изменилось. Как и ожидалось, элементы, связанные с удаленным списком, также были удалены Core Data. Это не волшебство. Core Data просто выполняет правила удаления, которые мы определили в модели данных.

Мы можем заключить, что отношения установлены правильно для этого типа приложения. Но что произойдет, если мы не настроим правила удаления правильно? Откройте модель данных и установите для правила удаления отношений, элементов и списка значение Нет действий . Запустите приложение еще раз и создайте несколько списков и элементов. Если вы удалите список и нажмете кнопку « Элементы» в левом верхнем углу, чтобы увидеть все элементы в постоянном хранилище, элементы, связанные с удаленным списком, все еще должны быть там. Они не были удалены, хотя список, к которому они принадлежат, был удален.

Нажмите на один из списков и посмотрите, что произойдет. Если вы запускаете приложение на iOS 8, то приложение будет аварийно завершено из-за необработанного исключения. Если вы запускаете приложение на iOS 9, то вы увидите только ошибку в консоли. Хватит чесать голову, и давайте разберемся вместе.

Несмотря на то, что приложение вылетает на iOS 8, вывод, который мы видим в консоли, ясен и точен.

1
Faulting[7189:2427906] *** Terminating app due to uncaught exception ‘NSObjectInaccessibleException’, reason: ‘CoreData could not fulfill a fault for ‘0x175b2ba0 <x-coredata://3920E0C9-6613-47E6-AA19-66AF6555140A/List/p1>»

Первое, на что нужно обратить внимание, это то, что приложение зависло из-за необработанного исключения. Однако для нашей дискуссии важнее причина, по которой было сгенерировано исключение. Базовые данные говорят нам, что он не смог выполнить ошибку для отношений. Отношение, к которому относится Базовые Данные, представляет собой отношение списка элемента, который мы коснулись в табличном представлении.

Если вы посмотрите на реализацию tableView(tableView:didSelectRowAtIndexPath:) , то вы поймете, почему Core Data tableView(tableView:didSelectRowAtIndexPath:) исключение. В этом методе мы выбираем элемент, который коснулся пользователь, и выводим имя списка на консоль.

1
2
3
4
5
6
7
8
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
     
    if let item = self.fetchedResultsController.objectAtIndexPath(indexPath) as?
        print(item.list?.name)
    }
     
}

Поскольку отношение списка является ошибкой, Core Data должна вызвать ошибку, чтобы устранить ее. Это означает, что Core Data запрашивает постоянное хранилище для записи списка. Проблема в том, что запись больше не находится в постоянном хранилище, поэтому возникло исключение.

Вплоть до iOS 9 это всегда было поведением по умолчанию. Начиная с iOS 9, Core Data больше не выдает исключение. Вместо этого он записывает загадочное сообщение на консоль. Несмотря на то, что сообщение неясно, оно все еще содержит причину проблемы.

1
Faulting[2806:1306995] CoreData: warning: An NSManagedObjectContext delegate overrode fault handling behavior to silently delete the object with ID ‘0xd000000000140000 <x-coredata://396CF04E-B8B4-4108-8719-3C651F880C33/List/p5>’ and substitute nil/0 for all property values instead of throwing.

Суть в том, что Core Data больше не выдает исключение, когда не может выполнить ошибку. Несколько хорошими новостями является то, что ваше приложение больше не падает, если вы не поймете исключение.

Причина, по которой не генерируется исключение в iOS 9, связана с введением нового свойства shouldDeleteInaccessibleFaults и нового метода shouldHandleInaccessibleFault(_:forObjectID:triggeredByProperty:) в классе NSManagedObjectContext . Я не буду освещать эти дополнения в этом уроке.

Что нужно помнить, так это то, что iOS 9 по умолчанию устанавливает для shouldDeleteInaccessibleFaults значение true . Это означает, что недоступный управляемый объект помечается как удаленный, а его свойства обнуляются. Если для shouldDeleteInaccessibleFaults задано значение false , shouldDeleteInaccessibleFaults данные возвращаются к старому поведению, вызывая исключение.

Конечно, свойство shouldDeleteInaccessibleFaults идет рука об руку с shouldHandleInaccessibleFault(_:forObjectID:triggeredByProperty:) . Если вы переопределите этот метод, вы сможете более элегантно обрабатывать недоступные объекты.

Когда вы сталкиваетесь с исключением из-за неспособности Core Data выполнить ошибку, вы можете подумать, что Core Data немного агрессивна. Эта реакция довольно распространена для людей, плохо знакомых с рамками. Правда в том, что разработчик виноват. Базовые данные были разработаны с особым набором целей, и сбой является важным компонентом для достижения этих целей.

Несмотря на их имя, ошибки всегда должны быть выполнимы. Если сбой не может быть выполнен, это означает, что модель данных не настроена правильно, или приложение не соблюдает правила, установленные в Базовой платформе данных.

В Руководстве по программированию основных данных Apple перечисляет ряд сценариев, которые могут привести к сбоям, которые больше не могут быть выполнены. Наиболее распространенными сценариями являются неправильно настроенные отношения (правила удаления) и удаление управляемого объекта, в то время как приложение все еще имеет сильную ссылку на этот управляемый объект.

Будьте осторожны, есть и другие возможные сценарии. По моему опыту, разработчики часто сталкиваются с похожими проблемами из-за проблемы со стеком Core Data. Например, если приложение сохраняет строгую ссылку на управляемый объект и в какой-то момент освобождает контекст управляемого объекта этого управляемого объекта, то Core Data больше не в состоянии выполнить любые ошибки для этого управляемого объекта. Я надеюсь, вы понимаете, что этого не должно произойти, если вы играете по правилам Core Data.

Сбои в работе Core Data невероятно полезны и являются ключевым компонентом постоянной структуры Apple. Я надеюсь, что эта статья научила вас, как бороться с ошибками и где искать, если вы столкнетесь с ошибками, которые не могут быть устранены. Если у вас есть какие-либо вопросы, не стесняйтесь оставлять их в комментариях ниже или обращайтесь ко мне в Twitter .