Advertisement
  1. Code
  2. Mobile Development
  3. iOS Development

Core Data и Swift: Управляемые объекты и fetch-запросы

Scroll to top
Read Time: 14 min
This post is part of a series called Core Data and Swift.
Core Data and Swift: Data Model
Core Data and Swift: Relationships and More Fetching

() translation by (you can also view the original English article)

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

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

Необходимые условия

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

1. Управляемые объекты

Экземпляр NSManagedObject представляет собой запись резервного хранилища Core Data. Запомните, не имеет значения, как выглядит это резервное хранилище. Однако, возвращаясь к аналогии с базой данных, экземпляр NSManagedObject содержит информацию строки в таблице базы данных.

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

NSEntityDescription

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

NSManagedObjectContext

Управляемый объект также связан с экземпляром NSManagedObjectContext. Контекст управляемого объекта, к которому относится управляемый объект, следит за изменениями этого управляемого объекта.

2. Создание записи

С учетом вышесказанного, создание управляемого объекта делается довольно незамысловато. Чтобы убедиться в том, что управляемый объект настроен должным образом, рекомендуется использовать специальный инициализатор для создания новых экземпляров NSManagedObject. Давайте посмотрим, как происходит создание нового объекта person.

Откройте проект из предыдущей статьи или клонируйте репозиторий с GitHub. Так как мы не делаем полнофункционального приложения в этой статье, будем делать большую часть нашей работы в классе делегата приложения, AppDelegate. Откройте AppDelegate.swift и обновите реализацию application(_:didFinishLaunchingWithOptions:), как показано ниже.

1
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
2
    // Create Managed Object

3
    let entityDescription = NSEntityDescription.entityForName("Person", inManagedObjectContext: self.managedObjectContext)
4
    let newPerson = NSManagedObject(entity: entityDescription!, insertIntoManagedObjectContext: self.managedObjectContext)
5
    
6
    return true
7
}

Первое, что мы делаем, - создаем экземпляр класса NSEntityDescription путем вызова entityForName(_:inManagedObjectContext:). Мы передаем имя сущности для которой хотим создать управляемый объект - "Person" и экземпляр NSManagedObjectContext.

Почему нам нужно передавать еще и объект NSManagedObjectContext? Мы указываем имя для управляемого объекта, который мы хотим создать, но мы также должны сказать Core Data, где можно найти модель данных для этой сущности. Помните, что контекст управляемого объекта прочно связан с координатором постоянного хранилища, который хранит ссылку на модель данных. Когда мы передаем контекст управляемого объекта, Core Data запрашивает у координатора постоянного хранилища модель данных, чтобы найти сущность, которая нам нужна.

На втором шаге мы вызываем назначенный инициализатор класса NSManagedObject, init(entity:insertIntoManagedObjectContext:). Мы передаем описание сущности и экземпляр NSManagedObjectContext. Что? Почему мы должны снова передавать экземпляр NSManagedObjectContext? Вспомните, что я писал ранее. Управляемый объект связан с описанием сущности и она живет в контексте управляемого объекта, именно поэтому мы сообщаем Core Data с каким контекстом управляемого объекта должны быть связан новый управляемый объект.

Это не слишком сложно. Да? Теперь мы создали новый объект person. Как нам изменить его атрибуты или установить взаимосвязь? Это делается с помощью метода ключ-значение (key-value). Чтобы изменить имя [first] нового объекта person, который мы только что создали, мы сделаем следующее:

1
// Configure New Person

2
newPerson.setValue("Bart", forKey: "first")
3
newPerson.setValue("Jacobs", forKey: "last")

Если вы знакомы с методом ключ-значение, то это должно выглядеть очень похоже. Поскольку класс NSManagedObject соответствует протоколу NSKeyValueCoding, мы устанавливаем атрибут, вызывая setValue(_:forKey:). Это так просто.

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

Прежде чем мы продолжим наше изучение NSManagedObject, давайте установить атрибуту age  объекта newPerson значение 44.

1
newPerson.setValue(44, forKey: "age")

3. Сохранение записи

Несмотря на то, что у нас теперь есть новый экземпляр person, Core Data еще не сохранила этот объект в свое резервное хранилище. На данный момент управляемый объект, который мы создали, живет только в контексте управляемого объекта, куда он был добавлен. Чтобы сохранить объект person в резервное хранилище, нам нужно сохранить изменения контекста управляемого объекта путем вызова save() у него.

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

1
do {
2
    try newPerson.managedObjectContext?.save()
3
} catch {
4
    print(error)
5
}

Постройте и запустите приложение, чтобы увидеть, что все работает так, как ожидалось. У вас тоже аварийное завершение? Что вам говорит консоль вывода? Это похоже на результат, что ниже?

1
Core Data[8560:265446] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unacceptable type of value for attribute: property = "first"; desired type = NSDate; given type = Swift._NSContiguousString; value = Bart.'
2
*** First throw call stack:
3
(
4
  0   CoreFoundation                      0x000000010c3f1f45 __exceptionPreprocess + 165
5
  1   libobjc.A.dylib                     0x000000010e118deb objc_exception_throw + 48
6
  2   CoreData                            0x000000010bf8d840 _PFManagedObject_coerceValueForKeyWithDescription + 2864
7
  3   CoreData                            0x000000010bf660d1 _sharedIMPL_setvfk_core + 177
8
  4   Core Data                           0x000000010be82200 _TFC9Core_Data11AppDelegate11applicationfS0_FTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVSs10DictionaryCSo8NSObjectPSs9AnyObject____Sb + 624
9
  5   Core Data                           0x000000010be82683 _TToFC9Core_Data11AppDelegate11applicationfS0_FTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVSs10DictionaryCSo8NSObjectPSs9AnyObject____Sb + 179
10
  6   UIKit                               0x000000010cc07034 -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 272
11
  7   UIKit                               0x000000010cc081da -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 3415
12
  8   UIKit                               0x000000010cc0ead3 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1750
13
  9   UIKit                               0x000000010cc0bcb3 -[UIApplication workspaceDidEndTransaction:] + 188
14
  10  FrontBoardServices                  0x0000000110000784 -[FBSSerialQueue _performNext] + 192
15
  11  FrontBoardServices                  0x0000000110000af2 -[FBSSerialQueue _performNextFromRunLoopSource] + 45
16
  12  CoreFoundation                      0x000000010c31e011 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
17
  13  CoreFoundation                      0x000000010c313f3c __CFRunLoopDoSources0 + 556
18
  14  CoreFoundation                      0x000000010c3133f3 __CFRunLoopRun + 867
19
  15  CoreFoundation                      0x000000010c312e08 CFRunLoopRunSpecific + 488
20
  16  UIKit                               0x000000010cc0b605 -[UIApplication _run] + 402
21
  17  UIKit                               0x000000010cc1041d UIApplicationMain + 171
22
  18  Core Data                           0x000000010be8377d main + 109
23
  19  libdyld.dylib                       0x000000010ec3092d start + 1
24
  20  ???                                 0x0000000000000001 0x0 + 1
25
)
26
libc++abi.dylib: terminating with uncaught exception of type NSException

Xcode говорит нам, что он ожидал экземпляр NSDate для атрибута first, но мы передали String. Если вы откроете модель Core Data, которую мы создали в предыдущей статье, то увидите, что типом атрибута first действительно является Date. Измените его на String и запустить приложение еще раз.

Снова аварийное завершение? Несмотря на то, что это более сложная тема, важно понять, что происходит.

Совместимость моделей данных

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

1
Core Data[8879:267986] CoreData: error: -addPersistentStoreWithType:SQLite configuration:(null) URL:file:///Users/Bart/Library/Developer/CoreSimulator/Devices/A263775B-4D73-48C8-BD79-825E0BED5128/data/Containers/Data/Application/D7298848-FC36-46EF-8C35-F890F2DB0C89/Documents/SingleViewCoreData.sqlite options:(null) ... returned error Error Domain=NSCocoaErrorDomain Code=134100 "(null)" UserInfo={metadata={
2
    NSPersistenceFrameworkVersion = 640;
3
    NSStoreModelVersionHashes =     {
4
        Address = <268460b1 0507da45 f37f8fb5 b17628a9 a56beb9c 8666f029 4276074d 11160d13>;
5
        Person = <c9bed257 c4bca383 38cd682a 227f38a8 c1a5bb27 fb02932c 42c62714 47463637>;
6
    };
7
    NSStoreModelVersionHashesVersion = 3;
8
    NSStoreModelVersionIdentifiers =     (
9
        ""
10
    );
11
    NSStoreType = SQLite;
12
    NSStoreUUID = "818D6962-8576-4F35-A334-A1A470561950";
13
    "_NSAutoVacuumLevel" = 2;
14
}, reason=The model used to open the store is incompatible with the one used to create the store} with userInfo dictionary {
15
    metadata =     {
16
        NSPersistenceFrameworkVersion = 640;
17
        NSStoreModelVersionHashes =         {
18
            Address = <268460b1 0507da45 f37f8fb5 b17628a9 a56beb9c 8666f029 4276074d 11160d13>;
19
            Person = <c9bed257 c4bca383 38cd682a 227f38a8 c1a5bb27 fb02932c 42c62714 47463637>;
20
        };
21
        NSStoreModelVersionHashesVersion = 3;
22
        NSStoreModelVersionIdentifiers =         (
23
            ""
24
        );
25
        NSStoreType = SQLite;
26
        NSStoreUUID = "818D6962-8576-4F35-A334-A1A470561950";
27
        "_NSAutoVacuumLevel" = 2;
28
    };
29
    reason = "The model used to open the store is incompatible with the one used to create the store";
30
}
31
Core Data[8879:267986] Unresolved error Error Domain=YOUR_ERROR_DOMAIN Code=9999 "Failed to initialize the application's saved data" UserInfo={NSLocalizedDescription=Failed to initialize the application's saved data, NSLocalizedFailureReason=There was an error creating or loading the application's saved data., NSUnderlyingError=0x7fde6d9acc00 {Error Domain=NSCocoaErrorDomain Code=134100 "(null)" UserInfo={metadata={
32
    NSPersistenceFrameworkVersion = 640;
33
    NSStoreModelVersionHashes =     {
34
        Address = <268460b1 0507da45 f37f8fb5 b17628a9 a56beb9c 8666f029 4276074d 11160d13>;
35
        Person = <c9bed257 c4bca383 38cd682a 227f38a8 c1a5bb27 fb02932c 42c62714 47463637>;
36
    };
37
    NSStoreModelVersionHashesVersion = 3;
38
    NSStoreModelVersionIdentifiers =     (
39
        ""
40
    );
41
    NSStoreType = SQLite;
42
    NSStoreUUID = "818D6962-8576-4F35-A334-A1A470561950";
43
    "_NSAutoVacuumLevel" = 2;
44
}, reason=The model used to open the store is incompatible with the one used to create the store

Когда мы впервые запустили приложение, несколько минут назад, Core Data проверив модель данных, создал на ее основе для нас хранилище, - базу данных SQLite в данном случае. Core Data все же умен. Это дает гарантию того, что структура резервного хранилища и что модели данных совместимы. Это жизненно важно, убедиться, что мы получаем обратно из резервного хранилища то, что ожидаем, то, что отправили туда в первый раз.

Во время первой аварийного завершения мы заметили, что наша модель данных содержит ошибку, и мы изменили тип атрибута first с Date на String. Другими словами, мы изменили модель данных, несмотря на то, что Core Data уже создал хранилище для нас на основе неправильной модели данных.

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

Как нам решить эту проблему? Простое решение заключается в том, чтобы деинсталлировать приложение с устройства (симулятора) и запустить приложение снова. Однако, вы не сможете так сделать, если у вас уже есть приложение в App Store, которым пользуются люди. В этом случае следует использовать механизм миграции, который мы обсудим в одной из следующих статей.

Так как у нас нет миллионов пользователей нашего приложения, мы может безопасно его удалить с тестового устройства и запустить приложение еще раз. Если все прошло хорошо, новый объект person теперь безопасно хранится в хранилище, базе данных SQLite, созданном Core Data для нас.

Проверка резервного хранилища

Вы можете проверить, что операция сохранения работает,  заглянув внутрь базы данных SQLite. Если вы запускали приложения в эмуляторе, то перейдите к /Users/<USER>/Library/Developer/CoreSimulator/Devices/<DEVICE_ID>/data/Containers/Data/Application/<APPLICATION_ID>/Documents/SingleViewCoreData.sqlite. Так как расположение данных приложения изменяется с каждым выпуском Xcode, указанный выше путь действителен только для Xcode 7.

Откройте базу данных SQLite и проверьте таблицу с именем ZPERSON. Таблица должна иметь одну запись, ту, который мы добавили минуту назад.

Contents of the SQLite DatabaseContents of the SQLite DatabaseContents of the SQLite Database

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

4. Извлечение записей

Несмотря на то, что мы будем подробно рассматривать  NSFetchRequest в следующей статье, нам нужен класс NSFetchRequest , чтобы запросить у Core Data информацию об объектном графе, которым он управляет. Давайте посмотрим, как мы можем получить запись, которую мы добавили ранее с помощью NSFetchRequest.

1
// Initialize Fetch Request

2
let fetchRequest = NSFetchRequest()
3
4
// Create Entity Description

5
let entityDescription = NSEntityDescription.entityForName("Person", inManagedObjectContext: self.managedObjectContext)
6
7
// Configure Fetch Request

8
fetchRequest.entity = entityDescription
9
10
do {
11
    let result = try self.managedObjectContext.executeFetchRequest(fetchRequest)
12
    print(result)
13
    
14
} catch {
15
    let fetchError = error as NSError
16
    print(fetchError)
17
}

После инициализации fetch-запроса, мы создаем объект NSEntityDescription и присваеваем его свойству entity этот fetch-запрос. Как вы можете видеть, мы используем класс NSEntityDescription для того, чтобы сказать Core Data, какая сущность нам интересна.

Выборку данных выполняет класс NSManagedObjectContext. Мы вызываем executeFetchRequest(_:), передав туда наш fetch-запрос. Так как executeFetchRequest(_:) является throw-методом [то есть может привести к аварийному завершению работы приложения], мы "заворачиваем" его вызов в оператор do-catch.

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

Запустите приложение и проверьте вывод консоли в Xcode. Ниже вы можете увидеть, что было возвращено в виде массива с одним объектом типа NSManagedObject. Сущность объекта - Person [человек].

1
[<NSManagedObject: 0x7fab71e0cee0> (entity: Person; id: 0xd000000000040000 <x-coredata://E9E9FE9D-D000-4F1D-BF2C-F37CEDF5FC39/Person/p1> ; data: <fault>)]

Для доступа к атрибутам записи, мы используем метод ключ-значение (как делали ранее). Важно хорошо усвоить работу с методом ключ-значение, если вы планируете работать с Core Data.

1
do {
2
    let result = try self.managedObjectContext.executeFetchRequest(fetchRequest)
3
    
4
    if (result.count > 0) {
5
        let person = result[0] as! NSManagedObject
6
        
7
        print("1 - \(person)")
8
        
9
        if let first = person.valueForKey("first"), last = person.valueForKey("last") {
10
            print("\(first) \(last)")
11
        }
12
        
13
        print("2 - \(person)")
14
    }
15
    
16
} catch {
17
    let fetchError = error as NSError
18
    print(fetchError)
19
}

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

1
1 - <NSManagedObject: 0x7f930b924210> (entity: Person; id: 0xd000000000040000 <x-coredata://E9E9FE9D-D000-4F1D-BF2C-F37CEDF5FC39/Person/p1> ; data: <fault>)
2
Bart Jacobs
3
2 - <NSManagedObject: 0x7f930b924210> (entity: Person; id: 0xd000000000040000 <x-coredata://E9E9FE9D-D000-4F1D-BF2C-F37CEDF5FC39/Person/p1> ; data: {
4
    addresses = "<relationship fault: 0x7f930b924150 'addresses'>";
5
    age = 44;
6
    first = Bart;
7
    last = Jacobs;
8
})

В первый раз, когда мы выводим объект person в консоль, мы видим, data: <fault> [значение отсутствует] Второй раз, однако, данные уже содержат атрибуты и взаимосвязи объекта. Почему так? Это все связано с faulting, ключевой концепцией Core Data.

5. Faulting

Концепция, лежащая в основе faulting [наличие  отсутствующих  значений], не уникальна для Core Data. Если вы когда-нибудь работали с Active Record на Ruby on Rails, то следующее будет безусловно знакомо. Концепции не являются идентичными, но очень похожи с точки зрения разработчика.

Core Data старается удерживать в памяти настолько мало, насколько это возможно. Это одна из используемых стратегий faulting. Когда мы извлекли записи для сущности Person минуту назад, Core Data выполнил запрос и получение данных, но он не полностью инициализирует эти записи.

То, что мы получили обратно - fault,  заполнитель, представляющий запись. Это объект типа NSManagedObject и мы можем рассматривать его именно так. С помощью не полной инициализизации записи, Core Data сохраняет низкое потребление памяти Для нашего примера это незначительный эффект, но только представьте себе, что произойдет, если бы мы запросили десятки, сотни или даже тысячи записей.

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

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

6. Обновление записей

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

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

1
let person = result[0] as! NSManagedObject
2
3
person.setValue(54, forKey: "age")
4
5
do {
6
    try person.managedObjectContext?.save()
7
} catch {
8
    let saveError = error as NSError
9
    print(saveError)
10
}

Вы можете убедиться в успешном обновлении [записи], взглянув на хранилище SQLite, как мы делали ранее.

Updating a Record in the Backing StoreUpdating a Record in the Backing StoreUpdating a Record in the Backing Store

7. Удаление записей

Удаление записи по той же схеме. О том, что запись надо удалить из постоянного хранилища мы сообщаем контексту управляемого объекта путем вызова deleteObject(_:) и передачи ему управляемого объекта, который необходимо удалить.

Удалите в нашем проекте объект person, который мы получили ранее, передав его в метод контекста управляемого объекта deleteObject(_:). Обратите внимание, что операция удаления не будет передана в резервное хранилище, до тех пор, пока мы вызовем save() у контексте управляемого объекта.

1
let person = result[0] as! NSManagedObject
2
3
self.managedObjectContext.deleteObject(person)
4
5
do {
6
    try self.managedObjectContext.save()
7
} catch {
8
    let saveError = error as NSError
9
    print(saveError)
10
}

Вы можете убедиться, что операция удаления прошла успешно, снова взглянув на хранилище SQLite.

Deleting a Record from the Backing StoreDeleting a Record from the Backing StoreDeleting a Record from the Backing Store

Заключение

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

В следующем выпуске этой серии статей вы узнаете как для создавать и обновлять взаимосвязи, также мы более глубоко рассмотрим класс NSFetchRequest. Кроме того, мы начнем использовать NSPredicate и NSSortDescriptor чтобы сделать наши fetch-запросы гибкими, динамичными и мощными.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.