Статьи

Основные данные с нуля: отношения и больше выборки

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

Мы уже работали со связями в редакторе модели 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»];

Хотя наша модель данных не определяет отношения один-к-одному, вы изучили все, что вам нужно знать, чтобы работать с этим типом отношений. Работа с отношением «один к одному» идентична работе с атрибутами. Единственное отличие состоит в том, что значение, которое вы возвращаете из 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»];

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

Чтобы отсортировать записи, которые мы получаем из контекста управляемого объекта, мы используем класс 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 . Этот класс поможет нам управлять коллекцией записей, но вы поймете, что это намного больше, чем это.