Статьи

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

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

То, что я освещаю в этой серии о Core Data, применимо к iOS 7+ и OS X 10.10+, но основное внимание будет уделено iOS. В этой серии я буду работать с Xcode 7.1 и Swift 2.1. Если вы предпочитаете Objective-C, то я рекомендую прочитать мои предыдущие серии по платформе Core Data .

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

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

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

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