В предыдущей статье мы узнали о NSManagedObject
и о том, как легко создавать, читать, обновлять и удалять записи с использованием Core Data. Тем не менее, я не упомянул отношения в этой дискуссии. Помимо нескольких предостережений, о которых нужно знать, отношениями так же легко управлять, как и атрибутами. В этой статье мы сосредоточимся на отношениях и продолжим исследование NSFetchRequest
.
Предпосылки
То, что я освещаю в этой серии о Core Data, применимо к iOS 7+ и OS X 10.10+, но основное внимание будет уделено iOS. В этой серии я буду работать с Xcode 7.1 и Swift 2.1. Если вы предпочитаете Objective-C, то я рекомендую прочитать мои предыдущие серии по платформе Core Data .
1. Отношения
Мы уже работали со связями в редакторе модели Core Data, и поэтому то, что я собираюсь вам рассказать, будет звучать знакомо. Доступ к отношениям, как и к атрибутам, осуществляется с использованием кодирования ключ-значение. Помните, что модель данных, которую мы создали ранее в этой серии, определяет сущность Person и сущность Address . Человек связан с одним или несколькими адресами, а адрес связан с одним или несколькими людьми. Это отношения многие ко многим .
Чтобы получить адреса человека, мы просто вызываем valueForKey(_:)
для человека, экземпляр NSManagedObject
, и передаем адреса в качестве ключа. Обратите внимание, что адреса — это ключ, который мы определили в модели данных. Какой тип объекта вы ожидаете? Большинство новичков в Core Data ожидают отсортированный массив, но Core Data возвращает набор, который не отсортирован. Работа с наборами имеет свои преимущества, как вы узнаете позже.
Создание записей
Достаточно теории, откройте проект из предыдущей статьи или клонируйте его из GitHub . Давайте начнем с создания человека, а затем связать его с адресом. Чтобы создать человека, откройте AppDelegate.swift и обновите application(_:didFinishLaunchingWithOptions:)
как показано ниже.
01
02
03
04
05
06
07
08
09
10
11
12
|
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Create Person
let entityPerson = NSEntityDescription.entityForName(«Person», inManagedObjectContext: self.managedObjectContext)
let newPerson = NSManagedObject(entity: entityPerson!, insertIntoManagedObjectContext: self.managedObjectContext)
// Populate Person
newPerson.setValue(«Bart», forKey: «first»)
newPerson.setValue(«Jacobs», forKey: «last»)
newPerson.setValue(44, forKey: «age»)
return true
}
|
Это должно выглядеть знакомо, если вы читали предыдущую статью . Создание адреса выглядит примерно так, как вы можете видеть ниже.
1
2
3
4
5
6
7
|
// Create Address
let entityAddress = NSEntityDescription.entityForName(«Address», inManagedObjectContext: self.managedObjectContext)
let newAddress = NSManagedObject(entity: entityAddress!, insertIntoManagedObjectContext: self.managedObjectContext)
// Populate Address
newAddress.setValue(«Main Street», forKey: «street»)
newAddress.setValue(«Boston», forKey: «city»)
|
Поскольку каждый атрибут объекта Address помечен как необязательный , нам не нужно присваивать значение каждому атрибуту. В этом примере мы только устанавливаем атрибуты улицы и города записи .
Создание отношений
Чтобы связать newAddress
с newPerson
, мы вызываем valueForKey(_:)
, передавая addresses
в качестве ключа. Значение, которое мы передаем, является экземпляром NSSet
который содержит newAddress
. Взгляните на следующий блок кода для пояснения.
1
2
3
4
5
6
7
8
9
|
// Add Address to Person
newPerson.setValue(NSSet(object: newAddress), forKey: «addresses»)
do {
try newPerson.managedObjectContext?.save()
} catch {
let saveError = error as NSError
print(saveError)
}
|
Мы вызываем save()
в контексте управляемого объекта newPerson
для распространения изменений в постоянном хранилище. Помните, что вызов save()
для контекста управляемого объекта сохраняет состояние контекста управляемого объекта. Это означает, что newAddress
также записывается в резервное хранилище, а также отношения, которые мы только что определили.
Вы можете удивиться, почему мы не связали newPerson
с newAddress
, потому что мы определили обратную связь в модели данных. Core Data создает эти отношения для нас. Если отношение имеет обратную связь, то Core Data автоматически об этом позаботится. Вы можете убедиться в этом, запросив newAddress
для своих persons
.
Извлечение и обновление отношений
Обновление отношений тоже не сложно. Единственное предостережение в том, что нам нужно добавить или удалить элементы из неизменяемых экземпляров Core Data NSSet
. Однако, чтобы упростить эту задачу, протокол NSKeyValueCoding
объявляет mutableSetValueForKey(_:)
метод mutableSetValueForKey(_:)
, который возвращает объект NSMutableSet
. Затем мы можем просто добавить или удалить элемент из коллекции, чтобы обновить отношения.
Взгляните на следующий блок кода, в котором мы создаем другой адрес и связываем его с newPerson
. Мы делаем это, вызывая mutableSetValueForKey(_:)
в newPerson
и добавляя otherAddress
в изменяемый набор. Нет необходимости сообщать Core Data, что мы обновили отношения. Core Data отслеживает изменяемый набор, который он нам дал, и обновляет отношения.
01
02
03
04
05
06
07
08
09
10
|
// Create Address
let otherAddress = NSManagedObject(entity: 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
let addresses = newPerson.mutableSetValueForKey(«addresses»)
addresses.addObject(otherAddress)
|
Удаление отношений
Вы можете удалить отношение, вызвав setValue(_:forKey:)
, передав значение nil
в качестве значения и имя отношения в качестве ключа. В следующем фрагменте кода мы отсоединяем каждый адрес от newPerson
.
1
2
|
// Delete Relationship
newPerson.setValue(nil, forKey:»addresses»)
|
2. Отношения один-к-одному и один-ко-многим
Индивидуальные отношения
Хотя наша модель данных не определяет отношения один-к-одному, вы изучили все, что вам нужно знать, чтобы работать с этим типом отношений. Работа с отношением «один к одному» идентична работе с атрибутами. Единственное отличие состоит в том, что значение, которое вы возвращаете из valueForKey(_:)
и значение, которое вы передаете setValue(_:forKey:)
является экземпляром NSManagedObject
.
Давайте обновим модель данных, чтобы проиллюстрировать это. Откройте Core_Data.xcdatamodeld и выберите сущность Person . Создайте новые отношения и назовите это супругом . Установите сущность Person в качестве пункта назначения и установите отношение супруга / супруга как обратное отношение.
Как видите, можно создать отношение, в котором местом назначения отношения является тот же объект, что и объект, который определяет отношение. Также обратите внимание, что мы всегда устанавливаем обратную связь. Как говорится в документации Apple , существует очень мало ситуаций, в которых вы хотите создать отношения, которые не имеют обратной связи.
Знаете ли вы, что произойдет, если вы должны были собрать и запустить приложение? Это верно, приложение будет зависать. Поскольку мы изменили модель данных, существующее резервное хранилище, в данном примере база данных SQLite, больше не совместимо с моделью данных. Чтобы исправить это, удалите приложение с вашего устройства или симулятора и запустите приложение. Не беспокойтесь, мы решим эту проблему более элегантно в следующей части, используя миграции.
Если вы можете запустить приложение без проблем, то пришло время для следующего шага. Вернитесь в класс делегата приложения и добавьте следующий блок кода.
1
2
3
4
5
6
7
|
// Create Another Person
let anotherPerson = NSManagedObject(entity: 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:)
для newPerson
и передаем в качестве аргументов anotherPerson
и "spouse"
. Мы можем достичь того же результата, вызвав setValue(_:forKey:)
для 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
let newChildPerson = NSManagedObject(entity: 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
let 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
let anotherChildPerson = NSManagedObject(entity: 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, forKey: «father»)
|
3. Больше выборки
Модель данных нашего примера приложения значительно выросла с точки зрения сложности. Мы создали отношения один-к-одному, один-ко-многим и многие-ко-многим. Мы видели, как легко создавать записи, включая отношения. Если мы также хотим получить эти данные из постоянного хранилища, нам нужно больше узнать о получении. Давайте начнем с простого примера, в котором мы видим, как сортировать результаты, возвращаемые запросом на выборку.
Дескрипторы сортировки
Чтобы отсортировать записи, которые мы получаем из контекста управляемого объекта, мы используем класс NSSortDescriptor
. Взгляните на следующий фрагмент кода.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
// Create Fetch Request
let fetchRequest = NSFetchRequest(entityName: «Person»)
// Add Sort Descriptor
let sortDescriptor = NSSortDescriptor(key: «first», ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
// Execute Fetch Request
do {
let result = try self.managedObjectContext.executeFetchRequest(fetchRequest)
for managedObject in result {
if let first = managedObject.valueForKey(«first»), last = managedObject.valueForKey(«last») {
print(«\(first) \(last)»)
}
}
} catch {
let fetchError = error as NSError
print(fetchError)
}
|
Мы инициализируем запрос на выборку, передавая интересующую нас сущность Person . Затем мы создаем объект NSSortDescriptor
, вызывая init(key:ascending:)
NSSortDescriptor
init(key:ascending:)
, передавая в первую очередь атрибут сущности, по которому мы хотим отсортировать, и логическое значение, указывающее, нужно ли сортировать записи в порядке возрастания или убывания.
Мы sortDescriptors
дескриптор сортировки с запросом выборки, устанавливая свойство sortDescriptors
запроса выборки. Поскольку свойство sortDescriptors
имеет тип [NSSortDescriptor]?
Можно указать более одного дескриптора сортировки. Мы рассмотрим этот вариант через минуту.
Остальная часть кода должна выглядеть знакомо. Запрос на выборку передается в контекст управляемого объекта, который выполняет запрос на выборку, когда мы вызываем executeFetchRequest(_:)
. Помните, что последний является методом throw, что означает, что мы используем ключевое слово try
и выполняем запрос на выборку в операторе do-catch
.
Запустите приложение и проверьте вывод в консоли XCode. Вывод должен выглядеть примерно так, как показано ниже. Как видите, записи отсортированы по имени.
1
2
3
4
|
Bart Jacobs
Jane Doe
Jim Doe
Lucy Doe
|
Если вы видите дубликаты в выводе, то обязательно закомментируйте код, который мы написали ранее, для создания записей. Каждый раз, когда вы запускаете приложение, создаются одни и те же записи, что приводит к дублированию записей.
Как я уже упоминал, можно комбинировать несколько дескрипторов сортировки. Давайте отсортируем записи по их фамилии и возрасту . Сначала мы устанавливаем ключ первого дескриптора сортировки на последний . Затем мы создаем другой дескриптор сортировки с ключом age и добавляем его в массив дескрипторов сортировки.
1
2
3
4
|
// Add Sort Descriptor
let sortDescriptor1 = NSSortDescriptor(key: «last», ascending: true)
let sortDescriptor2 = NSSortDescriptor(key: «age», ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor1, sortDescriptor2]
|
Вывод показывает, что порядок дескрипторов сортировки в массиве важен. Записи сначала сортируются по фамилии, а затем по возрасту.
1
2
3
4
|
Lucy Doe (19)
Jim Doe (21)
Jane Doe (42)
Bart Jacobs (44)
|
Предикаты
Дескрипторы сортировки хороши и просты в использовании, но предикаты — это то, что действительно делает выборку мощной в Core Data. Дескрипторы сортировки сообщают 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
26
|
// Fetching
let fetchRequest = NSFetchRequest(entityName: «Person»)
// Create Predicate
let predicate = NSPredicate(format: «%K == %@», «last», «Doe»)
fetchRequest.predicate = predicate
// Add Sort Descriptor
let sortDescriptor1 = NSSortDescriptor(key: «last», ascending: true)
let sortDescriptor2 = NSSortDescriptor(key: «age», ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor1, sortDescriptor2]
// Execute Fetch Request
do {
let result = try self.managedObjectContext.executeFetchRequest(fetchRequest)
for managedObject in result {
if let first = managedObject.valueForKey(«first»), last = managedObject.valueForKey(«last»), age = managedObject.valueForKey(«age») {
print(«\(first) \(last) (\(age))»)
}
}
} catch {
let fetchError = error as NSError
print(fetchError)
}
|
Мы не сильно изменились, за исключением создания объекта NSPredicate
путем вызова init(format:arguments:)
и привязки предиката к запросу на выборку, установив свойство predicate
последнего. Обратите внимание, что метод init(format:arguments:)
принимает переменное число аргументов.
Строка формата предиката использует %K
для имени свойства и %@
для значения. Как указано в Руководстве по программированию предикатов , %K
— это замена аргумента переменной для ключевого пути, а %@
— это замена аргумента переменной для значения объекта. Это означает, что строка формата предиката нашего примера оценивается как last == "Doe"
.
Если вы запустите приложение и проверьте вывод в консоли XCode, вы должны увидеть следующий результат:
1
2
3
|
Lucy Doe (19)
Jim Doe (21)
Jane Doe (42)
|
Есть много операторов, которые мы можем использовать для сравнения. В дополнение к =
и ==
, которые идентичны для базовых данных, есть также >=
и =>
, <=
и =>
!=
И <>
, а также >
и <
. Я рекомендую вам поэкспериментировать с этими операторами, чтобы узнать, как они влияют на результаты запроса на выборку.
Следующий предикат иллюстрирует, как мы можем использовать оператор >=
для выборки только записей Person с атрибутом age, превышающим 30 .
1
|
let predicate = NSPredicate(format: «%K >= %i», «age», 30)
|
У нас также есть операторы для сравнения строк, CONTAINS
, LIKE
, BEGINSWITH
, ENDSWITH
и ENDSWITH
. Давайте выберем каждую запись Person , имя которой CONTAINS
букву j
.
1
|
let predicate = NSPredicate(format: «%K CONTAINS %@», «first», «j»)
|
Если вы запустите приложение, массив результатов будет пустым, поскольку сравнение строк по умолчанию чувствительно к регистру. Мы можем изменить это, добавив модификатор следующим образом:
1
|
let predicate = NSPredicate(format: «%K CONTAINS[c] %@», «first», «j»)
|
Вы также можете создавать составные предикаты, используя ключевые слова AND
, OR
и NOT
. В следующем примере мы выбираем каждого человека, имя которого содержит букву j
и младше 30
.
1
|
let predicate = NSPredicate(format: «%K CONTAINS[c] %@ AND %K < %i», «first», «j», «age», 30)
|
Предикаты также позволяют легко получать записи на основе их отношений. В следующем примере мы выбираем каждого человека, чье имя отца равно Bart
.
1
|
let predicate = NSPredicate(format: «%K == %@», «father.first», «Bart»)
|
Приведенный выше предикат работает, как и ожидалось, потому что %K
является заменой аргумента переменной для ключевого пути , а не только для ключа .
Что вам нужно помнить, так это то, что предикаты позволяют вам запрашивать резервное хранилище, ничего не зная о магазине. Хотя синтаксис строки формата предиката в некотором роде напоминает SQL, не имеет значения, является ли хранилище резервных копий базой данных SQLite или хранилищем в памяти. Это очень мощная концепция, которая не уникальна для Core Data. Активная запись Rails — еще один прекрасный пример этой парадигмы.
Предикаты гораздо больше, чем то, что я показал вам в этой статье. Если вы хотите больше узнать о предикатах, я советую вам ознакомиться с Руководством по программированию предикатов от Apple. Мы также будем больше работать с предикатами в следующих статьях этой серии.
Вывод
Теперь вы хорошо знакомы с основами Core Data, и пришло время приступить к работе со средой, создав приложение, использующее возможности Core Data. В следующей статье мы познакомимся с другим важным классом платформы Core Data, NSFetchedResultsController
. Этот класс поможет нам управлять коллекцией записей, но вы поймете, что это намного больше, чем это.