Статьи

Базовые данные и Swift: управляемые объекты и запросы на выборку

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

Вы также познакомитесь с несколькими другими классами Core Data, такими как NSFetchRequest и NSEntityDescription . Позвольте мне начать с знакомства с NSManagedObject , вашим новым лучшим другом.

То, что я освещаю в этой серии о Core Data, применимо к iOS 7+ и OS X 10.10+, но основное внимание будет уделено iOS. В этой серии я буду работать с Xcode 7.1 и Swift 2.1. Если вы предпочитаете Objective-C, то я рекомендую прочитать мои предыдущие серии по платформе Core Data .

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

Причина, по которой Core Data использует NSManagedObject вместо NSObject качестве базового класса для моделирования записей, станет более понятной чуть позже. Прежде чем мы начнем работать с NSManagedObject , нам нужно знать кое-что об этом классе.

Каждый экземпляр NSManagedObject связан с экземпляром NSEntityDescription . Описание объекта включает в себя информацию об управляемом объекте, такую ​​как объект управляемого объекта, а также его атрибуты и отношения .

Управляемый объект также связан с экземпляром NSManagedObjectContext . Контекст управляемого объекта, к которому принадлежит управляемый объект, отслеживает изменения в управляемом объекте.

Учитывая вышесказанное, создание управляемого объекта довольно просто. Чтобы убедиться, что управляемый объект правильно настроен, рекомендуется использовать назначенный инициализатор для создания новых экземпляров NSManagedObject . Давайте посмотрим, как это работает, создав новый объект person.

Откройте проект из предыдущей статьи или клонируйте репозиторий из GitHub . Поскольку в этой статье мы не будем создавать функциональное приложение, большую часть нашей работы мы будем выполнять в классе делегатов приложения AppDelegate . Откройте AppDelegate.swift и обновите реализацию application(_:didFinishLaunchingWithOptions:) как показано ниже.

1
2
3
4
5
6
7
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Create Managed Object
    let entityDescription = NSEntityDescription.entityForName(«Person», inManagedObjectContext: self.managedObjectContext)
    let newPerson = NSManagedObject(entity: entityDescription!, insertIntoManagedObjectContext: self.managedObjectContext)
     
    return true
}

Первое, что мы делаем, это создаем экземпляр класса NSEntityDescription , вызывая entityForName(_:inManagedObjectContext:) . Мы передаем имя объекта, для которого мы хотим создать управляемый объект, "Person" и экземпляр NSManagedObjectContext .

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

На втором шаге мы вызываем указанный инициализатор класса NSManagedObject , init(entity:insertIntoManagedObjectContext:) . Мы передаем описание сущности и экземпляр NSManagedObjectContext . Подождите? Почему нам нужно передать другой экземпляр NSManagedObjectContext ? Помните, что я написал ранее. Управляемый объект связан с описанием объекта и находится в контексте управляемого объекта, поэтому мы сообщаем Core Data, с каким контекстом управляемого объекта должен быть связан новый управляемый объект.

Это не слишком сложно. Это? Теперь мы создали новый объект person. Как мы можем изменить его атрибуты или определить отношения? Это делается путем использования кодирования ключ-значение. Чтобы изменить имя нового объекта person, который мы только что создали, мы делаем следующее:

1
2
3
// Configure New Person
newPerson.setValue(«Bart», forKey: «first»)
newPerson.setValue(«Jacobs», forKey: «last»)

Если вы знакомы с кодированием значения ключа, то это должно выглядеть очень знакомо. Поскольку класс NSManagedObject соответствует протоколу NSKeyValueCoding , мы устанавливаем атрибут, вызывая setValue(_:forKey:) . Это так просто.

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

Прежде чем мы продолжим наше исследование NSManagedObject , давайте установим атрибут newPerson для newPerson 44 .

1
newPerson.setValue(44, forKey: «age»)

Несмотря на то, что у нас теперь есть новый экземпляр лица, Core Data еще не сохранила этого человека в своем резервном хранилище. В настоящий момент созданный нами управляемый объект живет только в контексте управляемого объекта, в который он был вставлен. Чтобы сохранить объект person в резервном хранилище, нам нужно сохранить изменения контекста управляемого объекта, вызвав для него функцию save() .

Метод save() является методом метания и возвращает логическое значение для указания результата операции сохранения. Взгляните на следующий блок кода для пояснения.

1
2
3
4
5
do {
    try newPerson.managedObjectContext?.save()
} catch {
    print(error)
}

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

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
Core Data[8560:265446] *** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘Unacceptable type of value for attribute: property = «first»;
*** First throw call stack:
(
  0 CoreFoundation 0x000000010c3f1f45 __exceptionPreprocess + 165
  1 libobjc.A.dylib 0x000000010e118deb objc_exception_throw + 48
  2 CoreData 0x000000010bf8d840 _PFManagedObject_coerceValueForKeyWithDescription + 2864
  3 CoreData 0x000000010bf660d1 _sharedIMPL_setvfk_core + 177
  4 Core Data 0x000000010be82200 _TFC9Core_Data11AppDelegate11applicationfS0_FTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVSs10DictionaryCSo8NSObjectPSs9AnyObject____Sb + 624
  5 Core Data 0x000000010be82683 _TToFC9Core_Data11AppDelegate11applicationfS0_FTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVSs10DictionaryCSo8NSObjectPSs9AnyObject____Sb + 179
  6 UIKit 0x000000010cc07034 -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 272
  7 UIKit 0x000000010cc081da -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 3415
  8 UIKit 0x000000010cc0ead3 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1750
  9 UIKit 0x000000010cc0bcb3 -[UIApplication workspaceDidEndTransaction:] + 188
  10 FrontBoardServices 0x0000000110000784 -[FBSSerialQueue _performNext] + 192
  11 FrontBoardServices 0x0000000110000af2 -[FBSSerialQueue _performNextFromRunLoopSource] + 45
  12 CoreFoundation 0x000000010c31e011 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
  13 CoreFoundation 0x000000010c313f3c __CFRunLoopDoSources0 + 556
  14 CoreFoundation 0x000000010c3133f3 __CFRunLoopRun + 867
  15 CoreFoundation 0x000000010c312e08 CFRunLoopRunSpecific + 488
  16 UIKit 0x000000010cc0b605 -[UIApplication _run] + 402
  17 UIKit 0x000000010cc1041d UIApplicationMain + 171
  18 Core Data 0x000000010be8377d main + 109
  19 libdyld.dylib 0x000000010ec3092d start + 1
  20 ???
)
libc++abi.dylib: terminating with uncaught exception of type NSException

Xcode сообщает нам, что ожидал экземпляр NSDate для первого атрибута, но мы передали String . Если вы откроете модель Core Data, которую мы создали в предыдущей статье, вы увидите, что тип первого атрибута действительно Date . Измените его на String и запустите приложение еще раз.

Еще один сбой? Несмотря на то, что это более сложная тема, важно понимать, что происходит.

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

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Core Data[8879:267986] CoreData: error: -addPersistentStoreWithType:SQLite configuration:(null) URL:file:///Users/Bart/Library/Developer/CoreSimulator/Devices/A263775B-4D73-48C8-BD79-825E0BED5128/data/Containers/Data/Application/D7298848-FC36-46EF-8C35-F890F2DB0C89/Documents/SingleViewCoreData.sqlite options:(null) … returned error Error Domain=NSCocoaErrorDomain Code=134100 «(null)» UserInfo={metadata={
    NSPersistenceFrameworkVersion = 640;
    NSStoreModelVersionHashes = {
        Address = <268460b1 0507da45 f37f8fb5 b17628a9 a56beb9c 8666f029 4276074d 11160d13>;
        Person = <c9bed257 c4bca383 38cd682a 227f38a8 c1a5bb27 fb02932c 42c62714 47463637>;
    };
    NSStoreModelVersionHashesVersion = 3;
    NSStoreModelVersionIdentifiers = (
        «»
    );
    NSStoreType = SQLite;
    NSStoreUUID = «818D6962-8576-4F35-A334-A1A470561950»;
    «_NSAutoVacuumLevel» = 2;
}, reason=The model used to open the store is incompatible with the one used to create the store} with userInfo dictionary {
    metadata = {
        NSPersistenceFrameworkVersion = 640;
        NSStoreModelVersionHashes = {
            Address = <268460b1 0507da45 f37f8fb5 b17628a9 a56beb9c 8666f029 4276074d 11160d13>;
            Person = <c9bed257 c4bca383 38cd682a 227f38a8 c1a5bb27 fb02932c 42c62714 47463637>;
        };
        NSStoreModelVersionHashesVersion = 3;
        NSStoreModelVersionIdentifiers = (
            «»
        );
        NSStoreType = SQLite;
        NSStoreUUID = «818D6962-8576-4F35-A334-A1A470561950»;
        «_NSAutoVacuumLevel» = 2;
    };
    reason = «The model used to open the store is incompatible with the one used to create the store»;
}
Core Data[8879:267986] Unresolved error Error Domain=YOUR_ERROR_DOMAIN Code=9999 «Failed to initialize the application’s saved data» UserInfo={NSLocalizedDescription=Failed to initialize the application’s saved data, NSLocalizedFailureReason=There was an error creating or loading the application’s saved data., NSUnderlyingError=0x7fde6d9acc00 {Error Domain=NSCocoaErrorDomain Code=134100 «(null)» UserInfo={metadata={
    NSPersistenceFrameworkVersion = 640;
    NSStoreModelVersionHashes = {
        Address = <268460b1 0507da45 f37f8fb5 b17628a9 a56beb9c 8666f029 4276074d 11160d13>;
        Person = <c9bed257 c4bca383 38cd682a 227f38a8 c1a5bb27 fb02932c 42c62714 47463637>;
    };
    NSStoreModelVersionHashesVersion = 3;
    NSStoreModelVersionIdentifiers = (
        «»
    );
    NSStoreType = SQLite;
    NSStoreUUID = «818D6962-8576-4F35-A334-A1A470561950»;
    «_NSAutoVacuumLevel» = 2;
}, reason=The model used to open the store is incompatible with the one used to create the store

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

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

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

Как мы решаем это? Простое решение — удалить приложение с устройства или из симулятора и снова запустить приложение. Однако это то, что вы не можете сделать, если у вас уже есть приложение в App Store, которое используют люди. В этом случае вы используете миграцию, о чем мы поговорим в следующей статье.

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

Вы можете убедиться, что операция сохранения сработала, заглянув в базу данных SQLite. Если вы запустили приложение в симуляторе, перейдите в / Users / <USER> / Library / Developer / CoreSimulator / Devices / <DEVICE_ID> / data / Containers / Data / Application / <APPLICATION_ID> / Documents / SingleViewCoreData .sqlite . Поскольку расположение данных приложения меняется с каждым выпуском XCode, указанный выше путь действителен только для XCode 7.

Откройте базу данных SQLite и осмотрите таблицу с именем ZPERSON . В таблице должна быть одна запись, которую мы вставили минуту назад.

Содержимое базы данных SQLite

Вы должны помнить две вещи. Во-первых, нет необходимости понимать структуру базы данных. Core Data управляет резервным хранилищем для нас, и нам не нужно понимать его структуру для работы с Core Data. Во-вторых, никогда не обращайтесь напрямую в магазин поддержки. Core Data отвечает за резервное хранилище, и мы должны это учитывать, если мы хотим, чтобы Core Data хорошо выполняла свою работу. Если мы начнем взаимодействовать с базой данных SQLite — или любым другим типом хранилища — нет гарантии, что Core Data продолжит функционировать должным образом. Короче говоря, Core Data отвечает за магазин, поэтому оставьте его в покое.

Несмотря на то, что мы NSFetchRequest рассмотрим NSFetchRequest в следующей статье, нам нужен класс NSFetchRequest чтобы запрашивать у Core Data информацию из графа объектов, которым он управляет. Давайте посмотрим, как мы можем извлечь запись, которую мы вставили ранее, используя NSFetchRequest .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
// Initialize Fetch Request
let fetchRequest = NSFetchRequest()
 
// Create Entity Description
let entityDescription = NSEntityDescription.entityForName(«Person», inManagedObjectContext: self.managedObjectContext)
 
// Configure Fetch Request
fetchRequest.entity = entityDescription
 
do {
    let result = try self.managedObjectContext.executeFetchRequest(fetchRequest)
    print(result)
     
} catch {
    let fetchError = error as NSError
    print(fetchError)
}

После инициализации запроса на выборку мы создаем объект NSEntityDescription и присваиваем его свойству объекта запроса на выборку. Как видите, мы используем класс NSEntityDescription чтобы сообщить Core Data, в каком объекте мы заинтересованы.

Выборка данных обрабатывается классом NSManagedObjectContext . Мы вызываем executeFetchRequest(_:) , передавая запрос на выборку. Поскольку executeFetchRequest(_:) — это метод выброса, мы заключаем вызов метода в оператор do-catch .

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

Запустите приложение и проверьте вывод в консоли XCode. Ниже вы можете увидеть, что было возвращено, массив с одним объектом типа NSManagedObject . Сущность объекта — Персона .

1
[<NSManagedObject: 0x7fab71e0cee0> (entity: Person; id: 0xd000000000040000 <x-coredata://E9E9FE9D-D000-4F1D-BF2C-F37CEDF5FC39/Person/p1> ; data: <fault>)]

Чтобы получить доступ к атрибутам записи, мы используем кодирование значения ключа, как мы делали ранее. Важно познакомиться с кодированием ключ-значение, если вы планируете работать с Core Data.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
do {
    let result = try self.managedObjectContext.executeFetchRequest(fetchRequest)
     
    if (result.count > 0) {
        let person = result[0] as!
         
        print(«1 — \(person)»)
         
        if let first = person.valueForKey(«first»), last = person.valueForKey(«last») {
            print(«\(first) \(last)»)
        }
         
        print(«2 — \(person)»)
    }
     
} catch {
    let fetchError = error as NSError
    print(fetchError)
}

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

1
2
3
4
5
6
7
8
1 — <NSManagedObject: 0x7f930b924210> (entity: Person; id: 0xd000000000040000 <x-coredata://E9E9FE9D-D000-4F1D-BF2C-F37CEDF5FC39/Person/p1> ; data: <fault>)
Bart Jacobs
2 — <NSManagedObject: 0x7f930b924210> (entity: Person; id: 0xd000000000040000 <x-coredata://E9E9FE9D-D000-4F1D-BF2C-F37CEDF5FC39/Person/p1> ; data: {
    addresses = «<relationship fault: 0x7f930b924150 ‘addresses’>»;
    age = 44;
    first = Bart;
    last = Jacobs;
})

При первом входе объекта person в консоль мы видим данные: <fault> . Однако во второй раз данные содержат содержимое атрибутов и связей объекта. Это почему? Это связано с ошибками , ключевой концепцией Core Data.

Концепция, лежащая в основе ошибок, не уникальна для Core Data. Если вы когда-либо работали с Active Record в Ruby on Rails, то следующее наверняка прозвенит. Концепция не идентична, но похожа с точки зрения разработчика.

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

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

Неисправности, как правило, не о чем беспокоиться. В тот момент, когда вы получаете доступ к атрибуту или взаимосвязи управляемого объекта, возникает ошибка, что означает, что базовые данные преобразуют ошибку в реализованный управляемый объект. Вы можете увидеть это в нашем примере, и это также причина того, что второй оператор log объекта person не выводит ошибку на консоль.

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

Обновление записей так же просто, как создание новой записи. Вы извлекаете запись, изменяете атрибут или отношение и сохраняете контекст управляемого объекта. Поскольку управляемый объект, запись, связан с контекстом управляемого объекта, последний знает о любых изменениях, вставках, обновлениях и удалениях. Когда контекст управляемого объекта сохраняется, все данные передаются в резервное хранилище с помощью Core Data.

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

01
02
03
04
05
06
07
08
09
10
let person = result[0] as!
 
person.setValue(54, forKey: «age»)
 
do {
    try person.managedObjectContext?.save()
} catch {
    let saveError = error as NSError
    print(saveError)
}

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

Обновление записи в бэк-магазине

Удаление записи происходит по той же схеме. Мы сообщаем контексту управляемого объекта, что запись должна быть удалена из постоянного хранилища, вызывая deleteObject(_:) и передавая управляемый объект, который необходимо удалить.

В нашем проекте удалите объект person, который мы извлекли ранее, передав его в метод deleteObject(_:) контекста управляемого объекта. Обратите внимание, что операция удаления не сохраняется в резервном хранилище, пока мы не вызовем save() в контексте управляемого объекта.

01
02
03
04
05
06
07
08
09
10
let person = result[0] as!
 
self.managedObjectContext.deleteObject(person)
 
do {
    try self.managedObjectContext.save()
} catch {
    let saveError = error as NSError
    print(saveError)
}

Вы можете убедиться, что операция удаления прошла успешно, еще раз посмотрев хранилище SQLite.

Удаление записи из бэк-магазина

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

В следующей части этой серии вы узнаете, как создавать и обновлять отношения, и мы подробно рассмотрим класс NSFetchRequest . Мы также начнем использовать NSPredicate и NSSortDescriptor чтобы сделать наши запросы выборки гибкими, динамичными и мощными.