Статьи

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

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

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

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

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

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
— (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Initialize Window
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
     
    // Configure Window
    [self.window setBackgroundColor:[UIColor whiteColor]];
    [self.window makeKeyAndVisible];
     
    // Create Managed Object
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@»Person» inManagedObjectContext:self.managedObjectContext];
    NSManagedObject *newPerson = [[NSManagedObject alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:self.managedObjectContext];
     
    return YES;
}

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

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

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

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

1
2
[newPerson setValue:@»Bart» forKey:@»first»];
[newPerson setValue:@»Jacobs» forKey:@»last»];

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

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

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

1
[newPerson setValue:@44 forKey:@»age»];

Если вы не знакомы с кодированием значения ключа, вы можете быть удивлены тем, что мы передали литерал NSNumber вместо целого числа, как мы определили в нашей модели данных. Метод setValue:forKey: принимает только объекты, без примитивов. Имейте это в виду.

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

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

1
2
3
4
5
6
NSError *error = nil;
 
if (![newPerson.managedObjectContext save:&error]) {
    NSLog(@»Unable to save managed object context.»);
    NSLog(@»%@, %@», error, error.localizedDescription);
}

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

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
Core Data[1218:38496] *** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘Unacceptable type of value for attribute: property = «first»;
*** First throw call stack:
(
  0 CoreFoundation 0x01f4d646 __exceptionPreprocess + 182
  1 libobjc.A.dylib 0x01bef8e3 objc_exception_throw + 44
  2 CoreData 0x00308e6e _PFManagedObject_coerceValueForKeyWithDescription + 3454
  3 CoreData 0x002db39d _sharedIMPL_setvfk_core + 205
  4 CoreData 0x00308096 -[NSManagedObject(_PFDynamicAccessorsAndPropertySupport) _setGenericValue:forKey:withIndex:flags:] + 54
  5 CoreData 0x002f735c _PF_Handler_Public_SetProperty + 108
  6 CoreData 0x002f72c5 -[NSManagedObject setValue:forKey:] + 181
  7 Core Data 0x00002beb -[TSPAppDelegate application:didFinishLaunchingWithOptions:] + 891
  8 UIKit 0x0066bb37 -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 291
  9 UIKit 0x0066c875 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 2920
  10 UIKit 0x0066fa33 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1507
  11 UIKit 0x00687eb8 __84-[UIApplication _handleApplicationActivationWithScene:transitionContext:completion:]_block_invoke + 59
  12 UIKit 0x0066e77e -[UIApplication workspaceDidEndTransaction:] + 29
  13 FrontBoardServices 0x04264f1f -[FBSWorkspace clientEndTransaction:] + 87
  14 FrontBoardServices 0x0426c4ed __53-[FBSWorkspaceClient _queue_handleTransactionBookEnd]_block_invoke + 49
  15 CoreFoundation 0x01e71f90 __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 16
  16 CoreFoundation 0x01e67133 __CFRunLoopDoBlocks + 195
  17 CoreFoundation 0x01e66898 __CFRunLoopRun + 936
  18 CoreFoundation 0x01e6622b CFRunLoopRunSpecific + 443
  19 CoreFoundation 0x01e6605b CFRunLoopRunInMode + 123
  20 UIKit 0x0066e095 -[UIApplication _run] + 571
  21 UIKit 0x006716e5 UIApplicationMain + 1526
  22 Core Data 0x0000394d main + 141
  23 libdyld.dylib 0x0250bac9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

Xcode сообщает нам, что ожидал экземпляр NSDate для first атрибута, но мы передали NSString . Если вы откроете модель Core Data, которую мы создали в предыдущей статье, вы увидите, что тип first атрибута действительно 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
Unresolved error Error Domain=NSCocoaErrorDomain Code=134100 «The operation couldn’t be completed. (Cocoa error 134100.)» UserInfo=0xcb17a30 {metadata={
    NSPersistenceFrameworkVersion = 508;
    NSStoreModelVersionHashes = {
        Address = <268460b1 0507da45 f37f8fb5 b17628a9 a56beb9c 8666f029 4276074d 11160d13>;
        Person = <68eb2a17 12dfaf41 510772c0 66d91b3d 7cdef207 4948ac15 f9ae22cc fe3d32f2>;
    };
    NSStoreModelVersionHashesVersion = 3;
    NSStoreModelVersionIdentifiers = (
        «»
    );
    NSStoreType = SQLite;
    NSStoreUUID = «EBB4C708-F933-4E74-8EE0-47F9972EE523»;
    «_NSAutoVacuumLevel» = 2;
}, reason=The model used to open the store is incompatible with the one used to create the store}, {
    metadata = {
        NSPersistenceFrameworkVersion = 508;
        NSStoreModelVersionHashes = {
            Address = <268460b1 0507da45 f37f8fb5 b17628a9 a56beb9c 8666f029 4276074d 11160d13>;
            Person = <68eb2a17 12dfaf41 510772c0 66d91b3d 7cdef207 4948ac15 f9ae22cc fe3d32f2>;
        };
        NSStoreModelVersionHashesVersion = 3;
        NSStoreModelVersionIdentifiers = (
            «»
        );
        NSStoreType = SQLite;
        NSStoreUUID = «EBB4C708-F933-4E74-8EE0-47F9972EE523»;
        «_NSAutoVacuumLevel» = 2;
    };
    reason = «The model used to open the store is incompatible with the one used to create the store»;
}

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

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

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

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

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

Вы можете убедиться, что операция сохранения сработала, заглянув в базу данных SQLite. Если вы запустили приложение в iOS Simulator, перейдите в ~ / <ПОЛЬЗОВАТЕЛЬ> / Библиотека / Поддержка приложений / iPhone Simulator / <VERSION> / <OS> / Applications / <ID> /Documents/Core_Data.sqlite . Чтобы сделать вашу жизнь проще, я рекомендую вам установить SimPholder , инструмент, который делает навигацию по вышеуказанному пути намного, намного проще. Откройте базу данных SQLite и осмотрите таблицу с именем ZPERSON . В таблице должна быть одна запись, которую мы вставили минуту назад.

Вы должны помнить две вещи. Во-первых, нет необходимости понимать структуру базы данных. 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
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
 
NSEntityDescription *entity = [NSEntityDescription entityForName:@»Person» inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
 
NSError *error = nil;
NSArray *result = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
 
if (error) {
    NSLog(@»Unable to execute fetch request.»);
    NSLog(@»%@, %@», error, error.localizedDescription);
     
} else {
    NSLog(@»%@», result);
}

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

Выборка данных обрабатывается классом NSManagedObjectContext , мы вызываем executeFetchRequest:error: передавая запрос на выборку и указатель на объект NSError . Метод возвращает массив результатов, если запрос на выборку выполнен успешно, и nil если возникла проблема. Обратите внимание, что Core Data всегда возвращает объект NSArray если запрос на выборку выполнен успешно, даже если мы ожидаем один результат или если Core Data не найдет подходящих записей.

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

1
2
3
Core Data[1588:613] (
    «<NSManagedObject: 0x1094352d0> (entity: Person; id: 0xd000000000040000 <x-coredata://384642FD-C6B8-4F90-993B-755C44AB84A9/Person/p1> ; data: <fault>)»
)

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

1
2
3
4
5
6
7
8
if (result.count > 0) {
    NSManagedObject *person = (NSManagedObject *)[result objectAtIndex:0];
    NSLog(@»1 — %@», person);
     
    NSLog(@»%@ %@», [person valueForKey:@»first»], [person valueForKey:@»last»]);
     
    NSLog(@»2 — %@», person);
}

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

1
2
3
4
5
6
7
8
Core Data[1659:613] 1 — <NSManagedObject: 0x109382980> (entity: Person; id: 0xd000000000040000 <x-coredata://384642FD-C6B8-4F90-993B-755C44AB84A9/Person/p1> ; data: <fault>)
Core Data[1659:613] Bart Jacobs
Core Data[1659:613] 2 — <NSManagedObject: 0x109382980> (entity: Person; id: 0xd000000000040000 <x-coredata://384642FD-C6B8-4F90-993B-755C44AB84A9/Person/p1> ; data: {
    addresses = «<relationship fault: 0x109380b20 ‘addresses’>»;
    age = 44;
    first = Bart;
    last = Jacobs;
})

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

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

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

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

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

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

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

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

01
02
03
04
05
06
07
08
09
10
NSManagedObject *person = (NSManagedObject *)[result objectAtIndex:0];
 
[person setValue:@30 forKey:@»age»];
 
NSError *saveError = nil;
 
if (![person.managedObjectContext save:&saveError]) {
    NSLog(@»Unable to save managed object context.»);
    NSLog(@»%@, %@», saveError, saveError.localizedDescription);
}

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

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

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

01
02
03
04
05
06
07
08
09
10
NSManagedObject *person = (NSManagedObject *)[result objectAtIndex:0];
 
[self.managedObjectContext deleteObject:person];
 
NSError *deleteError = nil;
 
if (![person.managedObjectContext save:&deleteError]) {
    NSLog(@»Unable to save managed object context.»);
    NSLog(@»%@, %@», deleteError, deleteError.localizedDescription);
}

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

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