Недавно я начала работать над большим проектом с использованием Core Data. Обычное дело, что люди на проектах меняются, опыт теряется, а нюансы забываются. Углубить всех в изучение конкретного фреймворка невозможно — у всех полно своих рабочих проблем. Поэтому я подготовила небольшую презентацию, из тех пунктов, которые считаю важными или недостаточно освещенными в туториалах. Делюсь со всеми и надеюсь, что это поможет писать эффективный код и не делать ошибок. Предполагается, что вы уже немного в теме.
Начну с банального.
Core Data – это фреймворк, который управляет и хранит данные в приложении. Можно рассматривать Core Data, как оболочку над физическим реляционным хранилищем, представляющую данные в виде объектов, при этом сама Core Data не является базой данных.

Для создания хранилища в приложении используются классы NSPersistentStoreCoordinator или NSPersistentContainer. NSPersistentStoreCoordinator создает хранилище указанного типа на основе модели, можно указать размещение и дополнительные опции. NSPersistentContainer можно использовать с IOS10, дает возможность создания с минимальным количеством кода.
Работает это следующим образом: если по указанному пути существует база данных, то координатор проверяет ее версию и, при необходимости, де��ает миграцию. Если база не существует, то она создается на основании модели NSManagedObjectModel. Чтобы все это работало правильно, перед внесением изменений в модель создавайте новую версию в Xcode через меню Editor -> Add Model Version. Если вывести путь, то можно найти и открыть базу в эмуляторе.
Core Data использует 4 типа хранилища:
— SQLite
— Binary
— In-Memory
— XML (только для Mac OS)
Если, например, по соображениям безопасности Вы не хотите хранить данные в файловом виде, но при этом хотите использовать кеширование в течении сессии и данные в виде объектов, вполне подойдет хранилище типа «In-Memory». Собственно, не запрещается иметь в одном приложении несколько хранилищ разного типа.
Несколько слов хочется сказать об объекте NSManagedObjectContext. Вообще, Apple дает весьма туманную формулировку для NSManagedObjectContext — среда для работы с объектами Core Data. Все это от желания отмежеваться от ассоциаций с реляционными базами, и представить Core Data, как простое в использовании средство, не требующее понимания ключей, транзакций и прочей базданской атрибутики. Но на языке реляционных баз NSManagedObjectContext можно, в некотором смысле, назвать менеджером транзакций. Вы, наверное, заметили, что он имеет методы save и rollback, хотя скорее всего вы пользуетесь только первым.
Недопонимание этого простого факта, приводит к использованию одноконтекстной модели, даже в тех ситуациях, где ее недостаточно. К примеру, вы редактируете большой документ, и при этом вам потребовалось загрузить пару справочников. В какой момент вызывать save? Если бы мы работали с реляционной базой, то тут бы не возникло вопросов, поскольку каждая операция выполнялась бы в своей транзакции. В Core Data тоже есть вполне удобный способ для решения этой проблемы — это ответвление дочернего контекста. Но к сожалению, это почему-то используется редко. Вот тут есть неплохая статья на эту тему.
По непонятной мне причине, существует очень большое количество мануалов и примеров, где никак не используется наследование для Entity/NSManagedObject(таблиц). Между тем, это очень удобный и��струмент. Если вы не используете наследование, то присваивать значения атрибутам (полям) можно только через KVС механизм, который, не выполняет проверку имен и типов атрибутов, а это легко может привести к ошибкам времени выполнения.
Переопределение классов для NSManagedObject делается в дизайнере Core Data:

После указания названия класса для Entity, можно воспользоваться кодогенерацией и получить класс с готовым кодом:


Если вы хотите посмотреть автогенерируемый код, но при этом, не хотите добавлять файлы в проект, можно воспользоваться другим способом: установить у Entity опцию «Codegen». В этом случае код нужно поискать в ../DerivedData/…

Используйте кодогенерацию для создания классов, опечатки в названии переменных могут привести к ошибкам времени выполнения.
Вот примерно такой код будет создан:
В swift @NSManaged имеет тот же смысл, что и dynamic в Objective C.
Core Data сама заботится о получении данных (имеет внутренние аксессоры) для атрибутов своих классов. Если у вас есть транзитные поля, то нужно добавлять функции для их расчета.
Классы, наследуемые, от NSManagedObject (таблицы), не имели до IOS10 «обычного» конструктора, в отличии от остальных классов. Чтобы создать объект типа Company, нужно было написать достаточно неповоротливую конструкцию с использованием NSEntityDescription. Сейчас появился более удобный метод инициализации через контекст (NSManagedObjectContext). Код ниже. Обратите внимание на преимущество наследования при присвоении атрибутов перед механизм KVC:
Еще одна вещь которая стоит отдельного упоминания — это пространство имен.

У вас не возникнет затруднений, если вы работаете на ObjectiveC или Swift. Обычно, это поле заполняется правильно по умолчанию. А вот в смешанных проектах, возможно, для вас станет сюрпризом, что для классов в swift и ObjectiveC нужно проставить разные опции. В Swift «Модуль» должен быть заполнен. Если это поле не будет заполнено, то к имени класса добавится префикс с названием проекта, что вызовет ошибку выполнения. В Objetive C «Модуль» оставляйте пустым, иначе NSManagedObject не будет найден при обращении к нему через имя класса.
В принципе тема связей неплохо освещена, но я хочу сделать акцент на способах добавления дочерних сущностей в родительскую. Поэтому сначала быстренько напомню механизм создания связей. Рассмотрим традиционный пример, компания — сотрудники, связь один ко многим:


Apple очень настаивает на указании инверсных связей. При этом, инверсия не усиливает связанность, а помогает Core Data отслеживать ��зменения на обоих сторонах связи, это важно для кеширования и обновления информации.
Также важно правильно указывать правило удаления. Правило удаления – это действие, которое будет выполнятся с данным объектом при удалении родительского объекта.
В указанном примере, при удалении компании все сотрудники будут удалены (cascade). При удалении сотрудника, ссылка на него в компании будет обнулена (пред экран)
1) Первый способ — это добавление через NSSet. Для примера добавим 2 сотрудника в компанию:
Этот способ удобен при первичной инициализации объекта или при заполнении базы. Тут есть небольшой нюанс. Если в компании уже были сотрудники, а вы присвоили новый set, то у бывших сотрудников обнулится ссылка на компанию, но они не будут удалены. Как вариант можно получить список сотрудников и работать уже с этим set'ом.
2) Добавление дочерних объектов через идентификатор родителя
Второй способ удобен при добавлении или редактировании дочернего объекта в
отдельной форме.
3) Добавление дочерних объектов через автогенерируемые методы
Для полноты картины полезно знать про этот метод, но мне он как-то не пригодился, а лишний код я удаляю, чтобы не загромождать проект.
В Core Data вы не можете составить произвольный запрос между любыми данными, как мы это можем делать в SQL. Но между зависимыми объектами это следить несложно используя стандартный предикат. Ниже пример запроса, выбирающего все компании в который есть сотрудник с указанным именем:
Вызов метода в коде будет выглядеть так:
Не используйте в запросах транзитные поля, их значения не определены в момент выполнения запроса. Ошибки не произойдет, но результат будет неправильный.
Вы, наверное заметили, что атрибуты Entity имеют несколько опций.
C опциональностью все понятно из названия.
Oпция «использовать скалярный тип» появилась в swif. В Objective C не используются скалярные типы для атрибутов, так как они не могут принимать значение nil. Попытка присвоить скалярное значение атрибуту через KVC вызовет ошибку выполнения. Отсюда становится понятным, почему типы атрибутов в Core Data не имеют строгого соответствия с типами языка. В swift, и в смешанных проектах атрибуты скалярного типа можно использовать.
Транзитные атрибуты – это расчетные поля, которые не сохраняются в базе. Их можно использовать для шифрования. Эти атрибуты получают значения через переопределенный аксессор, либо через присваивание примитивов по мере надобности (например, в переопределенных willSave и awakeFromFetch).
Если вам не нужно использовать расчетные поля, например, делать шифрование или что-то другое, то можно вообще не задумываться о том, чем являются аксессуары атрибутов. Между тем операции получения и присвоения значений атрибутам не являются «атомарными». Чтобы понять, что я имею ввиду смотрите код ниже:
Используйте примитивы в event’ах NSManagedObject вместо обычного присваивания, чтобы избежать зацикливания. Пример:
Если вдруг когда-то вам придется прикрутить в проект функцию awakeFromFetch, то вы удивитесь, что работает она весьма странно, а по факту вызывается она совсем не тогда, когда вы выполните запрос. Связано это с тем, что Core Data имеет весьма интеллектуальный механизм кеширования, и если выборка уже находится в памяти (например, по причине того, что вы только что заполнили эту таблицу), то метод вызываться не будет. Тем не менее, мои эксперименты показали, что в плане вычисляемых значений, можно смело положиться на использование awakeFromFetch, об этом же говорит документация Apple. Если же для тестирования и разработки вам нужно принудительно вызвать awakeFromFetch, добавьте перед запросом managedObjectContext.refreshAllObjects().
На этом все.
Спасибо всем, кто дочитал до конца.
Начну с банального.
Core Data – это фреймворк, который управляет и хранит данные в приложении. Можно рассматривать Core Data, как оболочку над физическим реляционным хранилищем, представляющую данные в виде объектов, при этом сама Core Data не является базой данных.
Объекты Core Data

Для создания хранилища в приложении используются классы NSPersistentStoreCoordinator или NSPersistentContainer. NSPersistentStoreCoordinator создает хранилище указанного типа на основе модели, можно указать размещение и дополнительные опции. NSPersistentContainer можно использовать с IOS10, дает возможность создания с минимальным количеством кода.
Работает это следующим образом: если по указанному пути существует база данных, то координатор проверяет ее версию и, при необходимости, де��ает миграцию. Если база не существует, то она создается на основании модели NSManagedObjectModel. Чтобы все это работало правильно, перед внесением изменений в модель создавайте новую версию в Xcode через меню Editor -> Add Model Version. Если вывести путь, то можно найти и открыть базу в эмуляторе.
Пример с NSPersistentStoreCoordinator
var persistentCoordinator: NSPersistentStoreCoordinator = {
let modelURL = Bundle.main.url(forResource: "Test", withExtension: "momd")
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL!)
let persistentCoordinator = NSPersistentStoreCoordinator(managedObjectModel:
managedObjectModel!)
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory,
.userDomainMask, true)[0]
let storeURL = URL(fileURLWithPath: documentsPath.appending("/Test.sqlite"))
print("storeUrl = \(storeURL)")
do {
try persistentCoordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: storeURL,
options: [NSSQLitePragmasOption:
["journal_mode":"MEMORY"]])
return persistentCoordinator
} catch {
abort()
}
} ()
Пример с NSPersistentContainer
var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "CoreDataTest")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
print("storeDescription = \(storeDescription)")
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
} ()
Core Data использует 4 типа хранилища:
— SQLite
— Binary
— In-Memory
— XML (только для Mac OS)
Если, например, по соображениям безопасности Вы не хотите хранить данные в файловом виде, но при этом хотите использовать кеширование в течении сессии и данные в виде объектов, вполне подойдет хранилище типа «In-Memory». Собственно, не запрещается иметь в одном приложении несколько хранилищ разного типа.
Несколько слов хочется сказать об объекте NSManagedObjectContext. Вообще, Apple дает весьма туманную формулировку для NSManagedObjectContext — среда для работы с объектами Core Data. Все это от желания отмежеваться от ассоциаций с реляционными базами, и представить Core Data, как простое в использовании средство, не требующее понимания ключей, транзакций и прочей базданской атрибутики. Но на языке реляционных баз NSManagedObjectContext можно, в некотором смысле, назвать менеджером транзакций. Вы, наверное, заметили, что он имеет методы save и rollback, хотя скорее всего вы пользуетесь только первым.
Недопонимание этого простого факта, приводит к использованию одноконтекстной модели, даже в тех ситуациях, где ее недостаточно. К примеру, вы редактируете большой документ, и при этом вам потребовалось загрузить пару справочников. В какой момент вызывать save? Если бы мы работали с реляционной базой, то тут бы не возникло вопросов, поскольку каждая операция выполнялась бы в своей транзакции. В Core Data тоже есть вполне удобный способ для решения этой проблемы — это ответвление дочернего контекста. Но к сожалению, это почему-то используется редко. Вот тут есть неплохая статья на эту тему.
Наследование
По непонятной мне причине, существует очень большое количество мануалов и примеров, где никак не используется наследование для Entity/NSManagedObject(таблиц). Между тем, это очень удобный и��струмент. Если вы не используете наследование, то присваивать значения атрибутам (полям) можно только через KVС механизм, который, не выполняет проверку имен и типов атрибутов, а это легко может привести к ошибкам времени выполнения.
Переопределение классов для NSManagedObject делается в дизайнере Core Data:

Наследование и кодогенерация
После указания названия класса для Entity, можно воспользоваться кодогенерацией и получить класс с готовым кодом:


Если вы хотите посмотреть автогенерируемый код, но при этом, не хотите добавлять файлы в проект, можно воспользоваться другим способом: установить у Entity опцию «Codegen». В этом случае код нужно поискать в ../DerivedData/…

Используйте кодогенерацию для создания классов, опечатки в названии переменных могут привести к ошибкам времени выполнения.
Вот примерно такой код будет создан:
@objc public class Company: NSManagedObject {
@NSManaged public var inn: String?
@NSManaged public var name: String?
@NSManaged public var uid: String?
@NSManaged public var employee: NSSet?
}
В swift @NSManaged имеет тот же смысл, что и dynamic в Objective C.
Core Data сама заботится о получении данных (имеет внутренние аксессоры) для атрибутов своих классов. Если у вас есть транзитные поля, то нужно добавлять функции для их расчета.
Классы, наследуемые, от NSManagedObject (таблицы), не имели до IOS10 «обычного» конструктора, в отличии от остальных классов. Чтобы создать объект типа Company, нужно было написать достаточно неповоротливую конструкцию с использованием NSEntityDescription. Сейчас появился более удобный метод инициализации через контекст (NSManagedObjectContext). Код ниже. Обратите внимание на преимущество наследования при присвоении атрибутов перед механизм KVC:
// 1 - создание записи через NSEntityDescription, присвоение значений через KVO
let company1 = NSEntityDescription.insertNewObject(forEntityName: "Company", into: moc)
company1.setValue("077456789111", forKey: "inn")
company1.setValue("Натура кура", forKey: "name")
// 2 - создание записи через NSEntityDescription, присвоение значений через свойства
let company2 = NSEntityDescription.insertNewObject(forEntityName: "Company", into: moc)
as! Company
company2.inn = "077456789222"
company2.name = "Крошка макарошка"
// 3 - создание записи через инициализатор (IOS10+), присвоение значений через свойства
let company3 = Company(context: moc)
company3.inn = "077456789222"
company3.name = "Крошка макарошка"
Пространство имен для NSManagedObject
Еще одна вещь которая стоит отдельного упоминания — это пространство имен.

У вас не возникнет затруднений, если вы работаете на ObjectiveC или Swift. Обычно, это поле заполняется правильно по умолчанию. А вот в смешанных проектах, возможно, для вас станет сюрпризом, что для классов в swift и ObjectiveC нужно проставить разные опции. В Swift «Модуль» должен быть заполнен. Если это поле не будет заполнено, то к имени класса добавится префикс с названием проекта, что вызовет ошибку выполнения. В Objetive C «Модуль» оставляйте пустым, иначе NSManagedObject не будет найден при обращении к нему через имя класса.
Связи между объектами
В принципе тема связей неплохо освещена, но я хочу сделать акцент на способах добавления дочерних сущностей в родительскую. Поэтому сначала быстренько напомню механизм создания связей. Рассмотрим традиционный пример, компания — сотрудники, связь один ко многим:
- Создаем связь на каждой стороне (таблице)
- После этого становится доступным поле Inverse, его нужно заполнить в каждой таблице.


Apple очень настаивает на указании инверсных связей. При этом, инверсия не усиливает связанность, а помогает Core Data отслеживать ��зменения на обоих сторонах связи, это важно для кеширования и обновления информации.
Также важно правильно указывать правило удаления. Правило удаления – это действие, которое будет выполнятся с данным объектом при удалении родительского объекта.
- Cascade — удаление всех дочерних объектов, при удалении родительского.
- Deny — запрет удаления родителя, если есть дочерний объект
- Nullify — обнуление ссылки на родителя
- No action — действие не указано, выдаст предупреждении при компиляции
В указанном примере, при удалении компании все сотрудники будут удалены (cascade). При удалении сотрудника, ссылка на него в компании будет обнулена (пред экран)
Способы добавления дочерних сущностей в родительскую
1) Первый способ — это добавление через NSSet. Для примера добавим 2 сотрудника в компанию:
let set = NSMutableSet();
if let employee1 = NSEntityDescription.insertNewObject(forEntityName: "Employee",
into: moc) as? Employee {
employee1.firstName = "Дима"
employee1.secondName = "Васильев"
set.add(employee1)
}
if let emploee2 = NSEntityDescription.insertNewObject(forEntityName: "Employee",
into: moc) as? Employee {
employee2.firstName = "Наташа"
employee2.secondName = "Ростова"
set.add(employee2)
}
company.employee = set
Этот способ удобен при первичной инициализации объекта или при заполнении базы. Тут есть небольшой нюанс. Если в компании уже были сотрудники, а вы присвоили новый set, то у бывших сотрудников обнулится ссылка на компанию, но они не будут удалены. Как вариант можно получить список сотрудников и работать уже с этим set'ом.
let set = company.mutableSetValue(forKey: "employee") 2) Добавление дочерних объектов через идентификатор родителя
if let employee = NSEntityDescription.insertNewObject(forEntityName: "Employee",
into: moc) as? Employee {
employee.firstName = "Маша"
employee.secondName = "Богданова"
employee.company = company
}
Второй способ удобен при добавлении или редактировании дочернего объекта в
отдельной форме.
3) Добавление дочерних объектов через автогенерируемые методы
extension Company {
@objc(addEmployeeObject:)
@NSManaged public func addEmployee(_ value: Employee)
@objc(removeEmployeeObject:)
@NSManaged public func removeFromEmployee(_ value: Employee)
@objc(addEmployee:)
@NSManaged public func addEmployee(_ values: NSSet)
@objc(removeEmployee:)
@NSManaged public func removeFromEmployee(_ values: NSSet)
}
Для полноты картины полезно знать про этот метод, но мне он как-то не пригодился, а лишний код я удаляю, чтобы не загромождать проект.
Запросы с условием по дочернему элементу
В Core Data вы не можете составить произвольный запрос между любыми данными, как мы это можем делать в SQL. Но между зависимыми объектами это следить несложно используя стандартный предикат. Ниже пример запроса, выбирающего все компании в который есть сотрудник с указанным именем:
public static func getCompanyWithEmployee(name: String) -> [Company] {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: self.className())
request.predicate = NSPredicate(format: "ANY employee.firstName = %@", name)
do {
if let result = try moc.fetch(request) as? [Company] {
return result
}
} catch { }
return [Company]()
}
Вызов метода в коде будет выглядеть так:
// выбрат�� комании, где работает Миша
let companies = Company.getCompanyWithEmployee(name: "Миша")
Не используйте в запросах транзитные поля, их значения не определены в момент выполнения запроса. Ошибки не произойдет, но результат будет неправильный.
Настройка атрибутов (полей)
Вы, наверное заметили, что атрибуты Entity имеют несколько опций.
C опциональностью все понятно из названия.
Oпция «использовать скалярный тип» появилась в swif. В Objective C не используются скалярные типы для атрибутов, так как они не могут принимать значение nil. Попытка присвоить скалярное значение атрибуту через KVC вызовет ошибку выполнения. Отсюда становится понятным, почему типы атрибутов в Core Data не имеют строгого соответствия с типами языка. В swift, и в смешанных проектах атрибуты скалярного типа можно использовать.
Транзитные атрибуты – это расчетные поля, которые не сохраняются в базе. Их можно использовать для шифрования. Эти атрибуты получают значения через переопределенный аксессор, либо через присваивание примитивов по мере надобности (например, в переопределенных willSave и awakeFromFetch).
Аксессоры атрибутов:
Если вам не нужно использовать расчетные поля, например, делать шифрование или что-то другое, то можно вообще не задумываться о том, чем являются аксессуары атрибутов. Между тем операции получения и присвоения значений атрибутам не являются «атомарными». Чтобы понять, что я имею ввиду смотрите код ниже:
// чтение
let name = company.name
// чтение
company.willAccessValue(forKey: "name")
let name = company.primitiveValue(forKey: "name")
company.didAccessValue(forKey: "name")
// присвоение
company.name = "Азбука укуса"
// присвоение
company.willChangeValue(forKey: "name")
company.setPrimitiveValue("Азбука укуса", forKey: "name")
company.didChangeValue(forKey: "name")
Используйте примитивы в event’ах NSManagedObject вместо обычного присваивания, чтобы избежать зацикливания. Пример:
override func willSave() {
let nameP = encrypt(field: primitiveValue(forKey: "name"), password: password)
setPrimitiveValue(nameP, forKey: "nameC")
super.willSave()
}
override func awakeFromFetch() {
let nameP = decrypt(field: primitiveValue(forKey: "nameC"), password: password)
setPrimitiveValue(nameP, forKey: "name")
super.awakeFromFetch()
}
Если вдруг когда-то вам придется прикрутить в проект функцию awakeFromFetch, то вы удивитесь, что работает она весьма странно, а по факту вызывается она совсем не тогда, когда вы выполните запрос. Связано это с тем, что Core Data имеет весьма интеллектуальный механизм кеширования, и если выборка уже находится в памяти (например, по причине того, что вы только что заполнили эту таблицу), то метод вызываться не будет. Тем не менее, мои эксперименты показали, что в плане вычисляемых значений, можно смело положиться на использование awakeFromFetch, об этом же говорит документация Apple. Если же для тестирования и разработки вам нужно принудительно вызвать awakeFromFetch, добавьте перед запросом managedObjectContext.refreshAllObjects().
На этом все.
Спасибо всем, кто дочитал до конца.
