Pull to refresh

Core Data для iOS. Глава №2. Теоретическая часть

Reading time 24 min
Views 34K
Хабралюди, добрый день!
Сегодня хочу начать написание ряда лекций с практическими заданиями по книге Михаеля Привата и Роберта Варнера «Pro Core Data for iOS», которую можете купить по этой ссылке. Каждая глава будет содержать теоретическую и практическую часть.



Содержание:
  • Глава №1. Приступаем (Практическая часть)
  • Глава №2. Усваиваем Core Data
  • Глава №3. Хранение данных: SQLite и другие варианты
  • Глава №4. Создание модели данных
  • Глава №5. Работаем с объектами данных
  • Глава №6. Обработка результатирующих множеств
  • Глава №7. Настройка производительности и используемой памяти
  • Глава №8. Управление версиями и миграции
  • Глава №9. Управление таблицами с использованием NSFetchedResultsController
  • Глава №10. Использование Core Data в продвинутых приложениях



Вступление


Многие разработчики, после первого знакомства с Core Data, считают его каким-то запутанным набором классов, который вместо того, чтобы упрощать работу с данными, наоборот, препятствует этому. Возможно это Rails разработчики, привыкшие к написанию динамических методов поиска и позволяющие принятым соглашениям выполнять за них всё грязную работу. Возможно это Java разработчик, который аннотирует Enterprise JavaBeans (EJB) и работает с Plain Old Java Objects (POJO). Кем бы ни были эти люди, чем бы они не занимались, они не воспринимают Core Data Framework таким какой он есть и его способы работы с данными, столько же разработчиков при виде того, как создаются «живые» интерфейсы с использованием Interface Builder кривятся и ухмыляются. Мы уверяем Вас, что Core Data это не очередная машина Рубена Голдберга. Классы в Core Data Framework скорее игроки Бостон Селтикс 1980 года и, когда вы поймете их подходы к игре, лишь тогда сможете оценить всю прелесть и законченность их игры и взаимодействия.

Знаете ли Вы?
Рубен Люциус Голдберг (англ. Reuben Lucius Goldberg; 1883—1970) — американский карикатурист, скульптор, писатель, инженер и изобретатель.
Голдберг более всего известен серией карикатур, в которых фигурирует так называемая «машина Руба Голдберга» — чрезвычайно сложное, громоздкое и запутанное устройство, выполняющее очень простые функции (например, огромная машина, занимающая целую комнату, цель которой — передвижение ложки с пищей от тарелки до рта человека).
В 1948 году Голдберг получил Пулитцеровскую премию за свои политические карикатуры, а в 1959 году — премию Banshees' Silver Lady Award.
Голдберг был одним из основателей и первым президентом Национального общества карикатуры. Его именем названа премия Рубена, которой организация награждает карикатурист года. В США ежегодно проходит конкурс машин Руба Голдберга.

image


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

Классы Core Data

В первой главе мы уже рассмотрели и создали самое простое приложение используя Core Data. Вы видели отрывки кода, какие классы были использованы, какие методы вызывались, в каком порядке этим самые методы вызывались и какие параметры этим методам передавались. Всё это для того делалось, чтобы понять каким образом работает Core Data. Вы слепо следовали тому, что вам говорили делать, возможно в некоторые моменты вы и недоумевали зачем это делается, но продолжали отстукивать на клавиатуре сладкие NSManagedObject, возможно вы задавали себе вопрос «А что будет, если сюда подставить другое значение?». Некоторые возможно даже попробовали заменить одно значение на другое, именно они получили что-то отличное от взрыва ;), а кто-то получил то, что и рассчитывал получить.

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

Знаете ли Вы?
Премия Тьюринга (англ. Turing Award) — самая престижная премия в информатике, вручаемая Ассоциацией вычислительной техники за выдающийся научно-технический вклад в этой области.

Премия учреждена Ассоциацией вычислительной техники в честь выдающегося английского учёного Алана Тьюринга, получившего первые глубокие результаты относительно вычислимости задолго до появления первых электронных вычислительных машин.
Премия ежегодно вручается одному или нескольким специалистам в области информатики и вычислительной техники, чей вклад в этой области оказал сильное и продолжительное влияние на компьютерное сообщество. Премия может быть присуждена одному человеку не более одного раза. В сфере информационных технологий премия Тьюринга имеет статус, аналогичный Нобелевской премии в академических науках. Впервые Премия Тьюринга была присуждена в 1966 году Алану Перлису за развитие технологии создания компиляторов.

В настоящее время премия спонсируется корпорациями Intel и Google и составляет 250 000 долларов США.

(с) Википедия


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

Откройте XCode и создайте Single View Application:
image

Назовите проект OrgChart.
Подключите в проект Core Data (см. Глава №1). Запустите приложение и убедитесь, что оно не падает.
image

На изображении приведенном ниже отображены классы Core Data с которыми мы обычно работаем:
image

В нашем коде сохранение данных происходит путём добавления новых NSManagedObject в NSManagedObjectContext, а получение данных с помощью NSFetchRequest класса. Как было показано в Главе №1, среда управления объектами (managed object context) создается и инициализируется при помощи диспетчера хранилищ данных (persistent store coordinator), реализуемого классом NSPersistentStoreCoordinator, который в свою очередь определяется моделью данных, реализуемой классом NSManagedObjectModel. Оставшаяся часть главы будет посвящена рассмотрению того, каким образом создаются эти классы, как они взаимодействуют между собой и, как их использовать.

Использующиеся при создании модели данных классы

Как было ранее упомянуто в Главе №1, все приложения использующие Core Data должны иметь объектную модель хранимых данных. Модель определяет сущности и их свойства. У сущности есть три типа свойств:
  • Атрибуты
  • Отношения
  • Свойства выборки


Таблица, которую вы видите ниже, показывает различные классы и описание их роли.
Перебирать классы для лучшего понимая механизма инициализации модели достаточно интересное занятие, но на практике создание модели в XCode требует от вас умения работать мышкой в графическом редакторе моделей, без написания единой строчки кода.
Наименование класса Роль
NSManagedObjectModel Модель данных
NSEntityDescription Сущность в модели данных
NSPropertyDescription Абстрактное описание свойства сущности
NSAttributeDescription Атрибут сущности
NSRelationshipDescription Ссылка одной сущности на другую
NSFetchedPropertyDescription Описание подмножества экземпляров сущностей выбранным по определенному критерию


Таблица ниже показывает отношения между классами использующимися при определении модели. NSManagedObjectContext не ссылается, либо ссылается на несколько объектов сущностей NSEntityDescription. Каждая объектная сущность NSEntityDescription не ссылается, либо ссылается на несколько объектов NSPropertyDescription. NSPropertyDescription является абстрактным классом с тремя конкретными реализациями:
  • NSAttributeDescription
  • NSRelationshipDescription
  • NSFetchedPropertyDescription

image

Этого маленького кол-ва классов будет достаточно для того, чтобы описать любую модель данных, которая будет вами разрабатываться с использованием Core Data Framework. Как описывалось ранее в Главе №1, для создания модели необходимо открыть XCode, выбрать пункт меню File -> New -> New File и указать «Data Model». В этой секции мы создадим модель, которая будет представлять собой организационную структуру компании.
Создайте новую модель в XCode и назовите её OrgChart. В этой модели данных у организации есть директор (CEO). Для простоты представим, что у человека есть два атрибута: уникальный идентификатор служащего и имя. Теперь мы готовы приступить к определению модели данных.
Откройте модель данных и создайте новую сущность Organization. Как и человек, организация определяется своим уникальным идентификатором и именем. Добавьте два атрибута сущности Organization. Атрибуты представляют собой постоянные свойства сущности, которые могут содержать значения некоторого типа. Типы данных атрибутов описываются в классе NSAttributeType и каждый тип данных налагает некоторые ограничения на сущность. Например, если вы попробуете записать строку в свойство с целочисленным типом данных, то возникнет ошибка.
Таблица ниже описывает существующие типы:
Тип атрибута в XCode Тип атрибута в Objective-C Objective-C Описание
Integer 16 NSInteger16AttributeType NSNumber 16-битное целое
Integer 32 NSInteger32AttributeType NSNumber 32-битное целое
Integer 64 NSInteger64AttributeType NSNumber 64-битное целое
Decimal NSDecimalAttributeType NSDecimalNumber Целочисленное значение по основанию 10
Double NSDoubleAttributeType NSNumber Объектная обёртка типа double
Float NSFloatAttributeType NSNumber Объектная обёртка типа float
String NSStringAttributeType NSString Строка символов
Boolean NSBooleanAttributeType BOOL Объектная обёртка логического типа
Date NSDateAttributeType NSDate Дата и время
Binary data NSBinaryDataAttributeType NSData Двоичные данные
Transformable NSTransformableAttributeType Любой нестандартный тип Любой тип, который может быть переведён в стандартный


Примечание
В Главе №5 мы разберем тип Transformable подробнее. Transformable-аттрибуты это способ уведомить Core Data о том, что будут использовать нестандартные типы данных и, в процессе сохранения данных в локальное хранилище, мы сообщим, каким образом преобразовать этот тип к одному из уже существующих встроенных.


Назовём первый атрибут id, а второй name. По умолчанию при создании нового атрибута его тип автоматически устанавливается в Undefined и не позволят проекту быть скомпилированным. Ваша задача выбрать подходящий тип для каждого из создаваемых вами атрибутов сущности. Идентификаторы организации всегда будут целочисленного типа, поэтому выберем Integer 16 в качестве типа для id атрибута. Тип String используем для атрибута name.
На данном этапе ваш проект должен выглядеть следующим образом:
image

Если бы на данном этапе программа была бы запущена, то объектный граф выглядел бы следующим образом:
image

На графе видно, что модель данных NSManagedObjectModel ссылается на объект сущности NSEntityDescription, который называется Organization и использует тип NSManagedObject для свойства managedObjectClassName. У сущности есть два атрибута. Каждый атрибут содержит два свойства: имя и тип. В первом атрибуте имя id и тип NSInteger16AttributeType для свойств name и attributeType соответственно.

Таким же образом создайте вторую сущность с именем Person и двумя атрибутами: id и name. Теперь мы можем создать связь между сущностями Organization и Person и назвать её leader. Создать связь достаточно просто: нажимаем на кнопку "+" в разделе «Relationships», выбираем «Destination» и «Source». Теперь добавим еще одну связь между Person и Person и назовём её employees. Последняя созданная нами связь — это связь типа «один к одному», в то время, как у служащего могут быть несколько подчиненных, а значит и тип связи необходим «один ко многим».
Откройте панель справа в XCode, как это показано на рисунке:
image

И поставьте галочку напротив пункта «Plural: To-Many Relationship»:
image

Модель данных выглядит следующим образом:
image

Вот как будет выглядеть объектный граф, когда модель будет загружена:
image
Данный граф не является графом объектов, которые Core Data хранит, это всего лишь отображение того, как выглядит модель в глазах Core Data.
Типичный способ загрузки модели данных Core Data выглядит следующим образом:
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"OrgChart" withExtension:@"momd"];

_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];


Знание того, каким образом представлены объекты модели в Core Data, в основном не очень полезное, разве что вы занимаетесь написанием собственного хранилища данных или заинтересованы в генерировании модели данных программно на этапе выполнения. Аналогию можно провести с созданием программно UIView, вместо того, чтобы использовать готовые элементы предоставляемые Interface Builder'ом. Однако глубокое понимание того, каким образом работает Core Data, позволит вам предсказывать и избегать сложностей, проблем, и приведёт к решению трудностей креативным и элегентным способом.

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

Классы доступа к данным

В Главе №1 мы выяснили, что инициализация Core Data начинается с загрузки модели данных в объект NSManagedObjectModel. В предыдущей секции было рассмотрено то, каким образом NSManagedObjectModel представляет внутреннюю структуру модели данных используя объекты NSPropertyDescription и NSEntityDescription. Следующим шагом в процессе инициализации Core Data является создание и связывание хранилища данных посредством NSPersistentStoreCoordinator. Наконец, последним шагом в процессе инициализации яляется создание NSManagedObjectContext объекта с которым непосредственно и происходит взаимодействие (чтение и запись данных).
Для того, чтобы прояснить всё, что было выше сказано, приведу диаграмму классов:
image

Как видим на графике есть и NSManagedObjectModel, которая загружается и затем передаётся NSPersistentStoreCoordinator для работы с хранилищами данных (да-да, их может быть несколько) и определения того, каким образом необходимо обрабатывать эти самые данные. Диспетчер хранилищ данных (persistent store coordinator) выступает в качестве такого себе дирижера оркестра, который координирует работу всех NSManagedObjectContext с хранилищем данных. Кроме всего прочего, диспетчер хранилищ данных отвечает за миграцию данных из одного хранилища в другое, достигается это использованием метода migratePersistentStore:.

NSPersistentStoreCoordinator инициализируется при помощи NSManagedObjectModel. После того, как наш диспетчер был создан, можно загружать и регистрировать хранилища данных. Приведенный ниже код показывает, каким образом можно осуществить инициализацию диспетчера хранилищ данных. Инициализация происходит посредством вызова метода initWithManagedObjectModel: и передаче модели данных, а затем происходит регистрация нового хранилища методом addPersistentStoreWithType:.

NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"OrgChart.sqlite"];

NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if(![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]){
   NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
   abort();
}


 iOS предоставляет три типа хранилищ:
Тип хранилища Описание
NSSQLiteStoreType База данных SQLite
NSBinaryStoreType Двоичный файл
NSInMemotyStoreType Хранилище данных во временной памяти устройства


Примечание
Core Data на Mac OS X предлагает еще и четвертый тип хранилища (NSXMLStoreType), который использует XML файл для хранения данных.


Типичным конечно же будет использование NSSQLiteStoreType, использование которого, свидетельствует о том, что данные будут храниться в SQLite базе данных. Но стоит всё таки быть знакомым и с другими типами хранилищ. Каким бы бесполезным не звучал тип NSInMemoryStoreType, типичный вариант использование данного типа хранилища: кэшировать информацию полученную от удаленного сервера и создавать локальную копию данных для того, чтобы снизить нагрузку на сеть и уменьшить кол-во гоняемого трафика вместе с задержками.
После того, как NSTersistentStoreCoodrinator инициализирован мы можем приступить к созданию и инициализации NSManagedObjectContext.
Среда управления объектами отвечает за то, что запрашивается из хранилища и что туда записывается. Задача может показаться просто, но вот вам несколько возможных трудностей:
  • Если два потока запрашивают один и тот же объект, то они должны получить указатель на один и тот же экземпляр объекта
  • Если объект запрашивается часто, то задача среды управления объектами использовать закэшированный объект, а не обращаться каждый раз к хранилищу
  • Среда управления объектами должна поддерживать неким образом изменения в хранилище до тех пор, пока не будет осуществлен запрос на сохранение данных


Примечание
Apple настоятельно рекомендует не наследовать свои классы от NSManagedObjectContext.


Таблица ниже содержит список основных методов для создания, получения и удаления объектов из NSManagedObjectContext. Измененить (обовить) объект можно путём изменения его свойств.
Наименование метода Описание метода
-executeFetchRequest:error: Осуществляет запрос на получение объектов
-objectWithID: Возвращает объект с указанным ижентификатором
-insertObject: Добавляет новый объект в среду
-deleteObject: Удаляет объект из среды


Стоит быть осторожным при удалении объектов и возможным последствиям этого действия. При удалении объекта, который связан с другими, Core Data необходимо принять решение, что делать с дочерними объектами. Одно из свойств отношений называется «Delete rule», которое может быть четырёх варинтов:
Наименование правила Эффект
No action Ничего не делает, позволяет считать дочерним объектам считать, что родительский объект существует
Nullify Для каждого дочернего объекта устанавливает ссылку на родительский объект в null
Cascade Удаляет каждый дочерний объект
Deny Запрещает удалять родительский объект, есть у него есть хотя бы один дочерний


В дополнение к тому, что среда управления объектами синхронизирует данные из хранилища, она еще позволяет осуществлять операции Undo и Redo, как это часто делается в текстовых редакторах и другом программном обеспечении.
Метод Описание
-undoManager Возвращает NSUndoManager, который отвечает за выполнение undo операций
-setUndoManager Устанавливает новый NSUndoManager
-undo Отправляет undo сообщение NSUndoManager
-redo Отправляет redo сообщение NSUndoManager
-reset Заставляет среду сбросить все указатели на объекты, которые она обрабатывает
-rollback Отправляет undo сообщение NSUndoManager до тех пор пока есть изменения
-save Сохраняет все изменения в локальном хранилище. Считай вызов этого метода нечто вроде коммита в Git. Все несохранненные данные будут потеряны, если приложение упадёт во время выполнения.
-hasChanges Возвращает YES, если в среде есть изменения, которые не были синхронизированы с хранилищем


В Главе №5 мы поговорим о undo и redo операциях в Core Data.

Экземпляры класса NSManagedObject поддерживают KVC для предоставления универсального способа доступа к данным, которые содержатся в объекте. Самый простой способ получить доступ к данным переменной экземпляра NSManagedObject это использовать метод valueForKey:, для записи (изменения) же данных воспользоваться методом setValue:forKey:. Параметр key является именем атрибута сущности модели.

- (id)valueForKey:(NSString *)key;
- (void)setValue:(id)value forKey:(NSString *)key;


Класс NSManagedObject так же предоставляет несколько вспомогательных методов в помощь NSManagedObjectContext для отслеживания изменений объекта. Когда приходит время осуществить «коммит» в хранилищу данных, то NSManagedObjectContext перебирает все объекты, которые содержит, и спрашивает изменилось ли их состояние.
Apple настоятельно не рекомендует переопределять методы, которые отвечают за состояние изменений для того, чтобы не нарушать всю цепочку действий при сохранении объектов.

Примечание
Каждый объект типа NSManagedObject содержит ссылку на среду (NSManagedObjectContext), которой он принадлежит. Это один из ненакладных вариантов получить ссылку на среду через объект, который в ней находится.


Как создавать новые NSManagedObject мы еще помним:
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Organization" inManagedObjectContext:self.managedObjectContext];
NSManagedObject *org = [NSEntityDescription insertEntityForName:[entity name] inManagedObjectContext:self.managedObjectContext];


После того, как мы создали новый объект в среде, можно приступить к установке значений его полей:
[org setValue:@"MyCompay, Inc." forKey:@"name"];
[org setValue:@77 forKey:@"id"];


KVO

Одним из интересных и удобных методов получения уведомления об изменении объекта является «подписка» на события изменений какого-либо свойства. NSManagedObject поддерживает KVO и берет на себя ответственность за то, чтобы слать уведомления об изменениях определенного свойства объекта. KVO это не что-то такое, что только из мира Core Data, этот паттерн активно используется в большинстве Cocoa фрэймворках. Два основных метода, которые относятся к KVO: willChangeValueForKey: и didChangeValueForKey:. Первый метод вызывается до того как произойдет изменения свойства, а второй соответственно после того.
Перед тем, как получать уведомления, необходимо зарегистрировать наш объект (который будет отслеживать изменения) в качестве наблюдателя. Вот как это может выглядеть:
[managedObject addObserver:observerObject forKeyPath:@"theProperty" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];

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

Классы запросов

На данном этапе мы уже знакомы с тем, каким образом происходит инициализация стэка Core Data и как взаимодействовать с созданными объектами (NSManagedObject). В это части мы поговорим о том, каким образом работать с запросами на получение данных из хранилища.
Как не странно, запросы на получение данных являются экземплярами класса NSFetchRequest. Самым интересным является тот факт, что NSFetchRequest является чуть ли не единственным классом ответственным за запрос (получение) данных в Core Data, остальные «вспомогательные» классы принадлежат Foundation Framework.
image

Запросы получения данных в основном состоят из двух частей: экземпляра NSPredicate и экземпляра NSSortDescriptor. NSPredicate предназначен для фильтрации данных по определенным критериям, а NSSortDescriptor отвечает за сортировку данных в определенном порядке (по возрастанию, убыванию). Оба эти элемента являются опциональными в запросе и, если ни один из них не будет указан, то вы получите набор всех данных в произвольном (неопределенном) порядке.

Создадим простой запрос на получение списка всех организаций без использования фильтров и сортировки:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Organization" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];

NSArray *organizations = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];

Так как мы не фильтровали список организаций, то в массиве organizations у нас будут все найденные в хранилище организации (объекты типа NSManagedObject).
Давайте получим только те организации в именах которых присутствует подстрока «Inc.»:
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name contains %@", @"Inc."];
[fetchRequest setPredicate:predicate];


Отсортируем полученное множество организаций по имени в лексикографическом порядке:
NSSortDescriptor *sortByName = [NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
[fetchRequest setSortDescriptors:@[sortByName]];


В Главе №6 мы детальней рассмотрим работу с NSPredicate и NSSortDescriptor.
Ниже представлен итоговый граф объектов Core Data:


Более детально мы рассмотрим NSFetchedResultsController в Главе №9.

Взаимодействие классов

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

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


Для каждой из сущностей мы добавили свойство id (уникальный идентификатор) типа Integer 16. Большинство программистов, наверно, которые дошли до этого момента уже подумали об автоинкрементировании и может быть даже пробовали искать что-то подобное в XCode. Увы и ах! Если вы один из таких программистов, то, наверно уже бросили попытки найти что-то подобное. А причина по которой вы не смогли этого найти простая — там этого просто нет! Core Data управляет объектным графом используя указатели на объекты. Нет смысла использовать уникальный идентификатор, как это обычно делается в реляционных базах данных вроде SQLite (да, возможно где-то Core Data и использует автоикрементируемые ключи, но нам-то об это не надо заботиться).
Мы намеренно ввели уникальный идентификатор в сущности по следующим причинам:
  • Поднять вопрос об автоинкрементировании
  • Рассмотреть пример работы с целочисленными типами


Персональный идентификатор является чем-то вроде социального страхового номера. Нет необходимости его автоматически инкрементировать. Что касается сущности Organization, то id будет являться хэшем объекта, хотя это и не даёт 100% гарантии уникальности, но для этого приложения подходит.

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


Сейчас мы немного вспомним шаги инициализации основных объектов Core Data.

OrgChartAppDelegate.h
#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>

@class OrgChartViewController;

@interface OrgChartAppDelegate : UIResponder <UIApplicationDelegate>

@property (nonatomic, strong) UIWindow *window;
@property (nonatomic, strong) OrgChartViewController *viewController;
@property (nonatomic, strong) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator;
@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;

- (void)saveContext;
- (NSURL *)applicationDocumentsDirectory;


Далее, в OrgChartAppDelegate.m реализовываем знакомые уже нам 3-4 метода:
- (void)saveContext 
{
   NSError *error = nil;
   NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
   if(nil != managedObjectContext) {
      if([managedObjectContext hasChanged] && ![managedObjectContext save:&error]){
         NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
         abort();
      }
   }
}

- (NSManagedObjectContext *)managedObjectContext
{
   if(nil != _managedObjectContext)
      return _managedObjectContext;

   NSPersistentStoreCoordinator *storeCoordinator = self.persistentStoreCoordinator;
   if(nil != storeCoordinator){
      _managedObjectContext = [[NSManagedObjectContext alloc] init];
      [_managedObjectContext setPersistentStoreCoordinator:storeCoordinator];
   }

   return _managedObjectContext;
}

- (NSManagedObjectModel *)managedObjectModel
{
   if(nil != _managedObjectModel)
      return _managedObjectModel;

   NSURL *model = [[NSBundle mainBundle] URLForResource:@"OrgChart" withExtension:@"momd"];
   _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:model];

   return _managedObjectModel;
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
   if(nil != _persistentStoreCoordinator)
      return _persistentStoreCoordinator;

   NSURL *storeURL = [[self applicationDocumentDirectory] URLByAppendingPathComponent:@"OrgChart.sqlite"];
   
   NSError *error = nil;
   _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initManagedObjectMode: self.managedObjectModel];
   if(![_persistentStoreCoordinator addStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]){
      NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
      abort();
   }

   return _persistentStoreCoordinator;
}

- (NSURL *)applicationDocumentsDirectory
{
   return [[[NSFileManaged defaultManager] URLsForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask] lastObject];
}

В этой главе мы сконцентрируемся на работе с Core Data, а не на реализации пользовательского интерфейса, поэтому вся работа будет осуществляться в методе application:didFinishLaunchingWithOptions:. Потом научимся с помощью внешнего инструментария проверять работу Core Data (сохранились ли данные в нашем локальном хранилище или нет).
Перед тем, как запрашивать какие-то данные из хранилище необходимо что-то туда добавить, поэтому изменим наш application:didFinishLaunchingWithOptions: следующим образом:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
   [self createData];
   
   self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

   self.viewController = [[OrgChartViewController alloc] initWithNibName:@"OrgChartViewController" bundle:nil];
   self.window.rootViewController = self.viewController;
   [self.window makeKeyAndVisible]

   return YES;
}


В OrgChartAppDelegate.h:
- (void)createData;


В OrgChartAppDelegate.m:
- (void)createData
{
   //....
}


Начнем с того, что создадим организацию:
NSManagedObject *organization = [NSEntityDescription insertNewObjectForEntityForName:@"Organization" inManagedObjectContext:self.managedObjectContext];

Мы создали организацию, но не указали ни её имени, ни идентификатора.
[organization setValue:@"MyCompany, Inc." forKey:@"name"];

Как мы помним, атрибут id у нас типа Integer 16. Все устанавливаемые значения в CoreData должны быть объектами (никаких int, long, char, enum). Воспользуемся классом обёрткой для целочисленного типа: NSNumber.
Код будет выглядеть следующим образом:
[organization setValue:[NSNumber numberWithInt:[@"MyCompany, Inc." hash]] forKey:@"id"];

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

Организация без людей не может работать, хотя…
Создадим нескольких сотрудников:
NSManagedObject *john = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:self.managedObjectContext];
[john setValue:@"John" forKey:@"name"];
[john setValue:[NSNumber numberWithInt:[@"John" hash]] forKey:@"id"];

NSManagedObject *jane = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:self.managedObjectContext];
[jane setValue:@"Jane" forKey:@"name"];
[jane setValue:[NSNumber numberWithInt:[@"Jane" hash]] forKey:@"id"];

NSManagedObject *bill = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:self.managedObjectContext];
[bill setValue:@"Bill" forKey:@"name"];
[bill setValue:[NSNumber numberWithInt:[@"Bill" hash]] forKey:@"id"];

Теперь у нас есть организация и три никак не связанных человека. Джон у нас СЕО, а Джейн и Билли помощники.
У нас есть два вида связи: один-к-одному (у организации только один СЕО) и один-ко-многим (у одного сотрудника может быть много подчиненных).
Так вот связать организацию (установить связь) с Джоном и сделать его СЕО достаточно просто:
[organization setValue:john forKey:@"leader"];

Core Data прекрасно понимает, какой объект вы ей подсунили и какая связь должна быть. Кстати, если попробуете вместо Джона передать какой-то другой тип, то код работать не будет.

Теперь надо разобраться, что делать с Джейн и Билли. Core Data возвращает из отношения один-ко-многим многое в виде типа NSSet. Поэтому, для того, чтобы у нас была возможность добавить новых сотрудников необходимо вызвать метод mutableSetValueForKey:, а не valueForKey:, который вернет неизменяемое множество объектов.
Вот так будет выглядеть код добавления Джейн и Билли:
NSMutableSet *johnsEmployees = [john mutableSetValueForKey:@"employees"];
[johnsEmployees addObject:jane];
[johnsEmployees addObject:bill];


Все эти изменения, весь этот граф объектов, которые мы создали надо сохранить:
[self saveContext];

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

SQLite

Берем лопату в руки, запускаем консоль и… поехали:
cd ~/Library/Application\ Support/iPhone\ Simulator

Найдем нашу базу данных:
find . -name "OrgChart.sqlite" –print

Нам выдаст что-то вроде такого:
./5.0/Applications/E8654A34-8EC4-4EAF-B531-00A032DD5977/Documents/OrgChart.sqlite

Запускаем:
sqlite3 ./5.0/Applications/E8654A34-8EC4-4EAF-B531-� 00A032DD5977/Documents/OrgChart.sqlite

На данном этапе перед нами шелл SQLite. Можем вводить команды.
SQLite version 3.7.5
Enter ".help" for instructions
Enter SQL statements terminated with a ";" 
sqlite>

Одной из интересных и полезных команд является команда .scheme, которая отобразит нам структуру таблиц созданных Core Data:
sqlite> .schema
CREATE TABLE ZORGANIZATION ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZID INTEGER, ZLEADER INTEGER, ZNAME VARCHAR );
CREATE TABLE ZPERSON ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZID INTEGER, Z2EMPLOYEES INTEGER, ZNAME VARCHAR );
CREATE TABLE Z_METADATA (Z_VERSION INTEGER PRIMARY KEY, Z_UUID VARCHAR(255), Z_PLIST BLOB);
CREATE TABLE Z_PRIMARYKEY (Z_ENT INTEGER PRIMARY KEY, Z_NAME VARCHAR, Z_SUPER INTEGER, Z_MAX INTEGER);
CREATE INDEX ZORGANIZATION_ZLEADER_INDEX ON ZORGANIZATION (ZLEADER);
CREATE INDEX ZPERSON_Z2EMPLOYEES_INDEX ON ZPERSON (Z2EMPLOYEES);
sqlite>

Много внутренних полей, кое-какие дополнительные таблицы, которые вы вряд ли найдете в XCode редакторе моделей. Можно заметить поле для того самого автоматически инкрементируемого ключа (INTEGER PRIMARY KEY).
Попробуем осуществить выборку всех имеющихся в базе организаций и сотрудников:
sqlite> select Z_PK, ZID, ZLEADER, ZNAME from ZORGANIZATION; 
1|-19904|2|MyCompany, Inc.
sqlite> select Z_PK, ZID, Z2EMPLOYEES, ZNAME from ZPERSON; 
1|6050|2|Jane
2|-28989||John
3|28151|2|Bill
sqlite>

Обратите внимание на 3 столбец в котором указан идентификатор родительского объекта, в данном случае у нас Билли и Джейн находятся под руководством Джона (уникальный идентификатор равен 2).

Для выхода из шелла:
sqlite> .quit


Запрашиваем данные

В этой главе мы записывали данные в SQLite хранилище с помощью Core Data, и использовали шелл SQLite для исследования структуры базы данных, которую создаёт Core Data и проверки факта сохранения данных.
Теперь мы будем запрашивать данные из хранилища с использованием того же Core Data. Открываем XCode, файл OrgChartAppDelegate.m.

Так как связь между сотрудниками является рекурсивной (одна сущность с двух сторон отношения), напишем метод, который позволит выводить список сотрудников, которые подчинены указанному c приятным и понятным форматированием:
- (void)displayPerson:(NSManagedObject *)person withIndentation:(NSString *)indentation
{
   NSLog(@"%@Name: %@", indentation, [person valueForKey:@"name"]);

   // увеличиваем значение для подуровней
   indentation = [NSString stringWithFormat:@"%@ ", indentation];
   
   NSSet *employees = [person valueForKey:@"employees"];
   id employee;
   NSEnumerator *it = [employees objectEnumerator];
   while((employee = [it nextObject]) != nil)
   {
      [self displayPerson:employee withIndentation:indentation];
   }
}

Теперь напишем метод выборки всех организаций, отображение наименования организации с его СЕО и всеми подчиненными:
- (void)readData
{
   NSManagedObjectContext *context = [self managedObjectContext];
   NSEntityDescription *orgEntity = [NSEntityDescription entityForName:@"Organization" inManagedObjectContext:context];

   NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
   [fetchRequest setEntity:orgEntity];

   NSArray *organizations = [context executeFetchRequest:request error:nil];

   id organization;
   NSEnumerator *it = [organizations objectEnumerator];
   while ((organization = [it nextObject]) != nil)
   {
      NSLog(@"Organization: %@", [organization valueForKey:@"name"]);

      NSManagedObject *ceo = [ogranization valueForKey:@"leader"];
      [self displayPerson:ceo withIndentation:@"  "];
   }
}

Не забываем добавить методы в OrgChartAppDelegate.h:
- (void)readData;
- (void)displayPerson:(NSManagedObject *)person withIndentation:(NSString *)indentation;

Теперь вернемся к методу application:didFinishLaunchingWithOptions: и вместо вызова метода createData вызовем readData:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{
   //[self createData];
   [self readData];

   self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];  
   self.viewController = [[OrgChartViewController alloc] initWithNibName:@"OrgChartViewController" bundle:nil]; 
   self.window.rootViewController = self.viewController; [self.window makeKeyAndVisible];
   
   return YES;
}

Запустите приложение и загляните в консоль:


Не бойтесь экспериментировать: добавьте новых сотрудников, удалите базу данных и пусть приложение её восстановит заново, создайте новые организации.
Вот здесь мы, например, добавили нового сотрудника, руководителем которого назначена Джейн:


Заключение

К текущему моменту для вас Core Data не должна уже выглядеть чем-то непонятным и страшным. В этой главе мы исследовали множество классов, которые Core Data использует для своих нужд, но с которыми мы очень-очень редко (точнее — никогда) будем работать. Использование Core Data напоминает вождение автомобиля без необходимости быть механиком. Простота структуры классов Core Data не должна вас вводить в заблуждение. Вы уже видели на что способен Core Data и, это лишь начало. Чем больше мы будем углубляться во множество настроек, связок с UI, альтернативных типов хранилищ, настраиваемых NSManagedObject's, тем четче вы сможете увидеть элегантность применяемых подходов к хранению данных в Core Data.
Tags:
Hubs:
+29
Comments 10
Comments Comments 10

Articles