В предыдущей статье мы узнали о NSManagedObject
и о том, как легко создавать, читать, обновлять и удалять записи с использованием Core Data. Тем не менее, я не упомянул отношения в этой дискуссии. Помимо нескольких предостережений, о которых нужно знать, отношениями так же легко управлять, как и атрибутами. В этой статье мы сосредоточимся на отношениях и продолжим исследование NSFetchRequest
.
1. Отношения
Мы уже работали со связями в редакторе модели Core Data, и поэтому то, что я собираюсь вам рассказать, будет звучать знакомо. Доступ к отношениям, как и к атрибутам, осуществляется с использованием кодирования ключ-значение. Помните, что модель данных, которую мы создали ранее в этой серии, определяет сущность Person и сущность Address . Человек связан с одним или несколькими адресами, а адрес связан с одним или несколькими людьми. Это отношения многие ко многим .
Чтобы получить адреса человека, мы просто вызываем valueForKey:
для человека, экземпляр NSManagedObject
, и передаем addresses
в качестве ключа. Обратите внимание, что addresses
— это ключ, который мы определили в модели данных. Какой тип объекта вы ожидаете? Большинство NSArray
в Core Data ожидают отсортированный NSArray
, но Core Data возвращает NSSet
, который не отсортирован. Работа с NSSet
имеет свои преимущества, о которых вы узнаете позже.
Создание записей
Достаточно теории, откройте проект из предыдущей статьи или клонируйте его из GitHub . Давайте начнем с создания человека, а затем связать его с адресом. Чтобы создать человека, обновите application:didFinishLaunchingWithOptions:
метод, как показано ниже.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
— (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 Person
NSEntityDescription *entityPerson = [NSEntityDescription entityForName:@»Person» inManagedObjectContext:self.managedObjectContext];
NSManagedObject *newPerson = [[NSManagedObject alloc] initWithEntity:entityPerson insertIntoManagedObjectContext:self.managedObjectContext];
// Set First and Lats Name
[newPerson setValue:@»Bart» forKey:@»first»];
[newPerson setValue:@»Jacobs» forKey:@»last»];
[newPerson setValue:@44 forKey:@»age»];
return YES;
}
|
Это должно выглядеть знакомо, если вы читали предыдущую статью. Создание адреса выглядит примерно так, как вы можете видеть ниже.
1
2
3
4
5
6
7
|
// Create Address
NSEntityDescription *entityAddress = [NSEntityDescription entityForName:@»Address» inManagedObjectContext:self.managedObjectContext];
NSManagedObject *newAddress = [[NSManagedObject alloc] initWithEntity:entityAddress insertIntoManagedObjectContext:self.managedObjectContext];
// Set First and Last Name
[newAddress setValue:@»Main Street» forKey:@»street»];
[newAddress setValue:@»Boston» forKey:@»city»];
|
Поскольку каждый атрибут объекта Address помечен как необязательный , нам не нужно присваивать значение каждому атрибуту. В приведенном выше примере мы только устанавливаем атрибуты street
и city
.
Создание отношений
Чтобы связать newAddress
с newPerson
, мы вызываем setValue:forKey:
передавая addresses
в качестве ключа. Значение, которое мы передаем, является NSSet
который содержит newAddress
. Взгляните на следующий блок кода для пояснения.
1
2
3
4
5
6
7
8
9
|
// Add Address to Person
[newPerson setValue:[NSSet setWithObject:newAddress] forKey:@»addresses»];
// Save Managed Object Context
NSError *error = nil;
if (![newPerson.managedObjectContext save:&error]) {
NSLog(@»Unable to save managed object context.»);
NSLog(@»%@, %@», error, error.localizedDescription);
}
|
Мы вызываем save:
в контексте управляемого объекта newPerson
для распространения изменений в постоянном хранилище. Помните, что вызов save:
в контексте управляемого объекта сохраняет состояние контекста управляемого объекта. Это означает, что newAddress
также записывается в резервное хранилище, а также отношения, которые мы только что определили.
Вы можете удивиться, почему мы не связали newPerson
с newAddress
, потому что мы определили обратную связь в нашей модели данных. Core Data создает эти отношения для нас. Если отношение имеет обратную связь, то Core Data автоматически об этом позаботится. Вы можете убедиться в этом, запросив объект newAddress
для его persons
.
Извлечение и обновление отношений
Обновление отношений тоже не сложно. Единственное предостережение в том, что нам нужно добавить или удалить элементы из неизменяемых экземпляров Core Data NSSet
. Однако, чтобы упростить эту задачу, NSManagedObject
объявляет mutableSetValueForKey:
метод mutableSetValueForKey:
который возвращает объект NSMutableSet
. Затем мы можем просто добавить или удалить элемент из коллекции, чтобы обновить отношения.
Взгляните на следующий блок кода, в котором мы создаем другой адрес и связываем его с newPerson
. Мы делаем это, вызывая mutableSetValueForKey:
on newPerson
и добавляя otherAddress
в изменяемый набор. Нет необходимости сообщать Core Data, что мы обновили отношения. Core Data отслеживает изменяемый набор, который он нам дал, и соответственно обновляет отношения.
01
02
03
04
05
06
07
08
09
10
|
// Create Address
NSManagedObject *otherAddress = [[NSManagedObject alloc] initWithEntity:entityAddress insertIntoManagedObjectContext:self.managedObjectContext];
// Set First and Last Name
[otherAddress setValue:@»5th Avenue» forKey:@»street»];
[otherAddress setValue:@»New York» forKey:@»city»];
// Add Address to Person
NSMutableSet *addresses = [newPerson mutableSetValueForKey:@»addresses»];
[addresses addObject:otherAddress];
|
Удаление отношений
setValue:forKey:
отношение так же просто, как вызвать setValue:forKey:
передав значение nil
в качестве значения и имя отношения в качестве ключа. Это newPerson
связь каждого адреса от newPerson
.
1
2
|
// Delete Relationship
[newPerson setValue:nil forKey:@»addresses»];
|
2. Отношения один-к-одному и один-ко-многим
Индивидуальные отношения
Хотя наша модель данных не определяет отношения один-к-одному, вы изучили все, что вам нужно знать, чтобы работать с этим типом отношений. Работа с отношением «один к одному» идентична работе с атрибутами. Единственное отличие состоит в том, что значение, которое вы возвращаете из valueForKey:
и значение, которое вы передаете setValue:forKey:
является экземпляром NSManagedObject
.
Давайте обновим нашу модель данных, чтобы проиллюстрировать это. Откройте Core_Data.xcdatamodeld и выберите сущность Person . Создайте новые отношения и назовите это супругом . Установите сущность Person в качестве пункта назначения и установите отношение супруга / супруга как обратное отношение.
Как видите, вполне возможно создать отношение, в котором местом назначения отношения является тот же объект, что и объект, который определяет отношение. Также обратите внимание, что мы всегда устанавливаем обратную связь. Как говорится в документации , существует очень мало ситуаций, в которых вы захотите создать отношение, которое не имеет обратной связи.
Знаете ли вы, что произойдет, если вы должны были собрать и запустить приложение? Это верно, приложение будет зависать. Поскольку мы изменили модель данных, существующее хранилище, в данном примере база данных SQLite, больше не совместимо с моделью данных. Чтобы исправить это, удалите приложение с вашего устройства или iOS Simulator и запустите приложение. Не беспокойтесь, мы решим эту проблему более элегантно в следующей части, используя миграции.
Если вы можете запустить приложение без проблем, то пришло время для следующего шага. Вернитесь в класс делегата приложения и добавьте следующий блок кода.
1
2
3
4
5
6
7
|
// Create Another Person
NSManagedObject *anotherPerson = [[NSManagedObject alloc] initWithEntity:entityPerson insertIntoManagedObjectContext:self.managedObjectContext];
// Set First and Last Name
[anotherPerson setValue:@»Jane» forKey:@»first»];
[anotherPerson setValue:@»Doe» forKey:@»last»];
[anotherPerson setValue:@42 forKey:@»age»];
|
Чтобы установить anotherPerson
как супруга newPerson
, мы вызываем setValue:forKey:
on newPerson
и передаем в качестве аргумента anotherPerson
и @"spouse"
. Мы можем достичь того же результата, вызвав setValue:forKey:
on anotherPerson
и передав в качестве аргументов newPerson
и @"spouse"
.
1
2
|
// Create Relationship
[newPerson setValue:anotherPerson forKey:@»spouse»];
|
Отношения один ко многим
Давайте закончим с рассмотрением отношений один ко многим. Откройте Core_Data.xcdatamodeld , выберите сущность Person и создайте связь с именем children . Установите для пункта назначения « Персона» , установите для типа значение « Много» и оставьте пока обратное отношение пустым.
Создайте еще одно отношение с именем « папа» , установите в качестве пункта назначения « Персона» и установите обратное отношение для детей . Это автоматически заполнит обратные отношения детей, которые мы оставили пустыми минуту назад. Теперь мы создали отношения один ко многим, то есть у отца может быть много детей, но у ребенка может быть только один отец.
Вернитесь к делегату приложения и добавьте следующий блок кода. Мы создаем еще одну запись Person , устанавливаем ее атрибуты и устанавливаем ее как дочерний newPerson
, запрашивая в Core Data изменяемый набор для ключевых дочерних newPerson
и добавляя новую запись в изменяемый набор.
01
02
03
04
05
06
07
08
09
10
11
|
// Create a Child Person
NSManagedObject *newChildPerson = [[NSManagedObject alloc] initWithEntity:entityPerson insertIntoManagedObjectContext:self.managedObjectContext];
// Set First and Last Name
[newChildPerson setValue:@»Jim» forKey:@»first»];
[newChildPerson setValue:@»Doe» forKey:@»last»];
[newChildPerson setValue:@21 forKey:@»age»];
// Create Relationship
NSMutableSet *children = [newPerson mutableSetValueForKey:@»children»];
[children addObject:newChildPerson];
|
Следующий кодовый блок выполняет тот же результат, устанавливая атрибут father
anotherChildPerson
. В результате newPerson
становится отцом anotherChildPerson
а anotherChildPerson
становится дочерним элементом newPerson
.
01
02
03
04
05
06
07
08
09
10
|
// Create Another Child Person
NSManagedObject *anotherChildPerson = [[NSManagedObject alloc] initWithEntity:entityPerson insertIntoManagedObjectContext:self.managedObjectContext];
// Set First and Last Name
[anotherChildPerson setValue:@»Lucy» forKey:@»first»];
[anotherChildPerson setValue:@»Doe» forKey:@»last»];
[anotherChildPerson setValue:@19 forKey:@»age»];
// Create Relationship
[anotherChildPerson setValue:newPerson forKeyPath:@»father»];
|
3. Больше выборки
Модель данных нашего примера приложения значительно выросла с точки зрения сложности. Мы создали отношения один-к-одному, один-ко-многим и многие-ко-многим. Мы видели, как легко создавать записи, включая отношения. Однако, если мы также хотим иметь возможность извлекать эти данные из постоянного хранилища, нам нужно больше узнать о получении. Давайте начнем с простого примера, в котором мы видим, как сортировать результаты, возвращаемые запросом на выборку.
Дескрипторы сортировки
Чтобы отсортировать записи, которые мы получаем из контекста управляемого объекта, мы используем класс NSSortDescriptor
. Взгляните на следующий фрагмент кода.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
// Fetching
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@»Person»];
// Add Sort Descriptor
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@»first» ascending:YES];
[fetchRequest setSortDescriptors:@[sortDescriptor]];
// Execute Fetch Request
NSError *fetchError = nil;
NSArray *result = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
if (!fetchError) {
for (NSManagedObject *managedObject in result) {
NSLog(@»%@, %@», [managedObject valueForKey:@»first»], [managedObject valueForKey:@»last»]);
}
} else {
NSLog(@»Error fetching data.»);
NSLog(@»%@, %@», fetchError, fetchError.localizedDescription);
}
|
Мы инициализируем запрос на выборку, передавая интересующую нас сущность Person . Затем мы создаем объект NSSortDescriptor
, вызывая sortDescriptorWithKey:ascending:
передавая сначала NSSortDescriptor
объекта, по sortDescriptorWithKey:ascending:
мы хотим отсортировать, и логическое значение, указывающее, нужно ли сортировать записи в порядке возрастания или убывания.
Мы setSortDescriptors:
дескриптор сортировки с запросом выборки, вызывая setSortDescriptors:
в запросе выборки, передавая массив, содержащий дескриптор сортировки. Поскольку setSortDescriptors:
принимает массив, можно передавать более одного дескриптора сортировки. Мы рассмотрим этот вариант через минуту.
Остальная часть кода должна выглядеть знакомо. Запрос на выборку передается в контекст управляемого объекта, который выполняет запрос на выборку, когда мы вызываем executeFetchRequest:error:
Важно всегда передавать указатель на объект NSError
чтобы знать, что пошло не так, если выполнение запроса на выборку завершится неудачно.
Запустите приложение и проверьте вывод в консоли XCode. Вывод должен выглядеть примерно так, как показано ниже. Как видите, записи отсортированы по имени.
1
2
3
4
|
Core Data[1080:613] Bart, Jacobs
Core Data[1080:613] Jane, Doe
Core Data[1080:613] Jim, Doe
Core Data[1080:613] Lucy, Doe
|
Если вы видите дубликаты в выводе, то обязательно закомментируйте код, который мы написали ранее, для создания записей. Каждый раз, когда вы запускаете приложение, создаются одни и те же записи, что приводит к дублированию записей.
Как я упоминал ранее, возможно объединение нескольких дескрипторов сортировки. Давайте отсортируем записи по их фамилии и возрасту . Сначала мы устанавливаем ключ первого дескриптора сортировки на last
. Затем мы создаем другой дескриптор сортировки с ключом age
и добавляем его в массив дескрипторов сортировки, который мы передаем в setSortDescriptors:
1
2
3
4
|
// Add Sort Descriptor
NSSortDescriptor *sortDescriptor1 = [NSSortDescriptor sortDescriptorWithKey:@»last» ascending:YES];
NSSortDescriptor *sortDescriptor2 = [NSSortDescriptor sortDescriptorWithKey:@»age» ascending:YES];
[fetchRequest setSortDescriptors:@[sortDescriptor1, sortDescriptor2]];
|
Вывод показывает, что порядок дескрипторов сортировки в массиве важен. Записи сначала сортируются по фамилии, а затем по возрасту.
1
2
3
4
|
Core Data[1418:613] Lucy, Doe (19)
Core Data[1418:613] Jim, Doe (21)
Core Data[1418:613] Jane, Doe (42)
Core Data[1418:613] Bart, Jacobs (44)
|
Предикаты
Дескрипторы сортировки хороши и просты в использовании, но предикаты — это то, что действительно делает выборку мощной в Core Data. В то время как дескрипторы сортировки сообщают Core Data, как нужно сортировать записи, предикаты сообщают, какие записи вас интересуют. Класс, с которым мы будем работать, — это NSPredicate
.
Давайте начнем с выборки каждого члена семьи Доу. Это очень легко сделать, и синтаксис напомнит некоторым из вас о SQL.
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
|
// Fetching
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@»Person»];
// Create Predicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:@»%K == %@», @»last», @»Doe»];
[fetchRequest setPredicate:predicate];
// Add Sort Descriptor
NSSortDescriptor *sortDescriptor1 = [NSSortDescriptor sortDescriptorWithKey:@»last» ascending:YES];
NSSortDescriptor *sortDescriptor2 = [NSSortDescriptor sortDescriptorWithKey:@»age» ascending:YES];
[fetchRequest setSortDescriptors:@[sortDescriptor1, sortDescriptor2]];
// Execute Fetch Request
NSError *fetchError = nil;
NSArray *result = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
if (!fetchError) {
for (NSManagedObject *managedObject in result) {
NSLog(@»%@, %@», [managedObject valueForKey:@»first»], [managedObject valueForKey:@»last»]);
}
} else {
NSLog(@»Error fetching data.»);
NSLog(@»%@, %@», fetchError, fetchError.localizedDescription);
}
|
Мы не сильно изменились, за исключением создания объекта NSPredicate
помощью вызова predicateWithFormat:
и привязки предиката к запросу выборки, передав его в качестве аргумента вызова setPredicate:
. Идея predicateWithFormat:
похожа на stringWithFormat:
в том, что она принимает переменное число аргументов.
Обратите внимание, что строка формата предиката использует %K
для имени свойства и %@
для значения. Как указано в Руководстве по программированию предикатов , %K
— это замена аргумента переменной для ключевого пути, а %@
— это замена аргумента переменной для значения объекта. Это означает, что строка формата предиката нашего примера оценивается как last == "Doe"
.
Если вы запустите приложение еще раз и проверите вывод в консоли XCode, вы должны увидеть следующий результат:
1
2
3
|
Core Data[1582:613] Lucy, Doe (19)
Core Data[1582:613] Jim, Doe (21)
Core Data[1582:613] Jane, Doe (42)
|
Есть много операторов, которые мы можем использовать для сравнения. В дополнение к =
и ==
, которые идентичны для базовых данных, есть также >=
и =>
, <=
и =>
!=
И <>
, а также >
и <
. Я рекомендую вам поэкспериментировать с этими операторами, чтобы узнать, как они влияют на результаты запроса на выборку.
Следующий предикат иллюстрирует, как мы можем использовать оператор >=
для выборки только записей Person с атрибутом age
превышающим 30
.
1
|
NSPredicate *predicate = [NSPredicate predicateWithFormat:@»%K >= %@», @»age», @(30)];
|
У нас также есть операторы для сравнения строк, CONTAINS
, LIKE
, BEGINSWITH
, ENDSWITH
и ENDSWITH
. Давайте выберем каждую запись Person, имя которой CONTAINS
букву j
.
1
|
NSPredicate *predicate = [NSPredicate predicateWithFormat:@»%K CONTAINS %@», @»first», @»j»];
|
Если вы запустите приложение сейчас, массив результатов будет пустым, так как сравнение строк по умолчанию чувствительно к регистру. Мы можем изменить это, добавив модификатор следующим образом:
1
|
NSPredicate *predicate = [NSPredicate predicateWithFormat:@»%K CONTAINS[c] %@», @»first», @»j»];
|
Вы также можете создавать составные предикаты, используя ключевые слова AND
, OR
и NOT
. В следующем примере мы выбираем каждого человека, имя которого содержит букву j
и младше 30
.
1
|
NSPredicate *predicate = [NSPredicate predicateWithFormat:@»%K CONTAINS[c] %@ AND %K < 30″, @»first», @»j», @»age», @(30)];
|
Предикаты также позволяют легко получать записи на основе их отношений. В следующем примере мы выбираем каждого человека, чье имя отца равно Bart
.
1
|
NSPredicate *predicate = [NSPredicate predicateWithFormat:@»%K == %@», @»father.first», @»Bart»];
|
Приведенный выше предикат работает, как и ожидалось, потому что %K
является заменой аргумента переменной для ключевого пути , а не только для ключа .
Что вам нужно помнить, так это то, что предикаты позволяют вам запрашивать резервное хранилище, ничего не зная о магазине. Хотя синтаксис строки формата предиката в некотором роде напоминает SQL, не имеет значения, является ли хранилище резервных копий базой данных SQLite или хранилищем в памяти. Это очень мощная концепция, которая не уникальна для Core Data. Активная запись Rails — еще один прекрасный пример этой парадигмы.
Предикаты гораздо больше, чем то, что я показал вам в этой статье. Если вы хотите больше узнать о предикатах, я советую вам ознакомиться с Руководством по программированию предикатов от Apple. Мы также будем больше работать с предикатами в следующих статьях этой серии.
Вывод
Теперь мы хорошо разбираемся в основах Core Data, и пришло время начать работу с фреймворком, создав приложение, использующее его мощь. В следующей статье мы познакомимся с другим важным классом платформы Core Data, NSFetchedResultsController
. Этот класс поможет нам управлять коллекцией записей, но вы поймете, что это намного больше, чем это.