CoreData модель из кода. Или «Как обойтись без .XCDataModel» (Часть 1)

от автора

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

Краткая предыстория

Я уже около полугода разрабатываю собственное приложение под iOS, и до недавнего времени процесс разработки шёл гладко и без особых проблем в области хранения данных.
Но недавно coreData-модель разрослась до неприличных с моей точки зрения размеров, и, так как я незрячий, и работать с большим объёмом полувизуальных данных мне физически тяжело, мной было принято решение перенести всё, что касается coreData в код.

Материала по "невизуальной" работе с coreData крайне мало, так что половина моих выводов и наработок основывается на безумных экспериментах в Playgroundах и не менее безумных попытках нагуглить недостающую информацию.

Вобщем, поехали!

Начнём с классов

Если при стандартной работе с coreData мы описываем все сущности и взаимосвязи между ними в отдельном файле с моделью, то в нашем случае для начала нам понадобятся классы, описывающие те самые сущности (так будет проще осознавать, с чем работаем, да и XCode будет помогать, подсказывая имена уже созданных классов).

Кстати, ниже будет использоваться странная приписка в конце каждого класса, "MO" — Managed Object. Это чтобы не перепутать с другими, возможно похожими, классами, но не относящимися к coreData.

Будем описывать следующую ситуацию:

  • Есть компания (описывается классом CompanyMO);
  • Есть сотрудники (описываются классом WorkerMO);
  • Каждый сотрудник может работать только в одной компании (или не работать вовсе);
  • Каждая компания может иметь любое количество сотрудников (в том числе и не иметь сотрудников вовсе).

@objc(CompanyMO) public class CompanyMO: NSManagedObject { @NSManaged public var companyName: String @NSManaged public var workers: NSSet }  @objc(WorkerMO) public class WorkerMO: NSManagedObject { @NSManaged public var firstname: String @NSManaged public var lastname: String @NSManaged public var company: CompanyMO? }

Теперь разберёмся с незнакомыми обозначениями.
objc() — возможно, вы видели подобную конструкцию около некоторых функций, но со скобками и именем класса между ними она встречается довольно редко.
objc(CompanyMO) перед объявлением класса CompanyMO говорит о том, что при использовании класса CompanyMO в среде Objective-C его имя будет CompanyMO. То есть objc(NameOfClass) указывает имя объявленного следом класса для среды Objective-C.

Следующая непонятность — @NSManaged перед каждым свойством класса.
@NSManaged обозначает, что инициализацией, значением и всем прочим для этой переменной будет управлять нечто уже в процессе выполнения кода. Я не силён в терминологии этой области, но знаю точно (с сайта Apple, кстати), что @NSManaged используется только в случае с coreData.

Подготовимся к созданию модели

Описание модели данных из кода — довольно некрасивая вещь, особенно с точки зрения количества написанного кода.
Поэтому перед тем, как описывать саму модель, мы добавим в код пару extensionов для более комфортной работы в будущем.

extension NSEntityDescription { convenience init(from classType: AnyClass) { self.init() self.name = NSStringFromClass(classType) self.managedObjectClassName = NSStringFromClass(classType) }  func addProperty(_ property: NSPropertyDescription) { self.properties.append(property) } }  extension NSAttributeDescription { convenience init(name: String, ofType: NSAttributeType, isOptional: Bool = false) { self.init() self.name = name self.attributeType = ofType self.isOptional = isOptional } }

А теперь разберёмся в выше написанном коде.
Класс NSEntityDescription — это класс coreData, описывающий сущность (компанию, сотрудника). У этого класса есть имя (то имя, с которым сущности этого класса будут храниться в базе) и имя класса (managedObjectClassName), этот атрибут должен содержать имя субкласса, описывающего модель (то, что мы писали выше — CompanyMO, WorkerMO и т д).
Так как нам не особо хочется, чтобы создание каждой сущности в модели выглядело вот так:

let company = NSEntityDescription() company.name = "CompanyMO" company.managedObjectClassName = "CompanyMO"

Мы сделали инициализатор, которому можем передать класс, описанный выше и на выходе получить готовый NSEntityDescription. Например, вот так:

let company = NSEntityDescription(from: CompanyMO.self) let worker = NSEntityDescription(from: WorkerMO.self)

Помоему, это красивее и удобнее, а главное выглядит логичнее и понятнее.

Перейдём к описанию самой модели

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

var model: NSManagedObjectModel { let _model = NSManagedObjectModel()  let companyEntity = NSEntityDescription(from: CompanyMO.self) companyEntity.addProperty(NSAttributeDescription(name: "companyName", ofType: .stringAttributeType)) // Здесь должно быть описание свойства CompanyMO.workers, но о взаимосвязях мы поговорим во второй части статьи let workerEntity = NSEntityDescription(from: WorkerMO.self) workerEntity.addProperty(NSAttributeDescription(name: "firstname", ofType: .stringAttributeType)) workerEntity.addPropertyNSAttributeDescription((name: "lastname", ofType: .stringAttributeType))  _model.entities = [companyEntity, workerEntity] return _model }

Разберём выше написанный код.
У объекта NSManagedObjectModel есть свойство entities, являющееся массивом объектов NSEntityDescription.

NSManagedObjectModel.entities: [NSEntityDescription]

Класс NSEntityDescription описывает сущность в базе данных. Если вы создаёте описание сущности, вы не создаёте объект этой сущности, не заполняете базу какими-либо данными. Вы просто сообщаете модели, что такая сущность есть, и с ней надо уметь работать.

У каждого NSEntityDescription объекта, в нашем случае companyEntity и workerEntity есть массив свойств companyEntity.properties и workerEntity.properties, который хранит в себе объекты NSPropertyDescription.

Класс NSPropertyDescription является родительским для NSAttributeDescription (описывает атрибуты или, проще говоря, свойства) и NSRelationshipDescription (описывает взаимосвязи между сущностями).
Про NSRelationshipDescription мы поговорим во второй части данной статьи.

Итак, мы делаем следующее:

  1. Создаём описания сущностей CompanyMO и WorkerMO;
  2. Добавляем в массив свойств каждой сущности необходимые объекты, описывающие свойства. Для companyEntity это companyName (про workers во второй части), а для workerEntity это firstname и lastname.
  3. Заполняем массив entities у создаваемой модели только что созданными и "наполненными" сущностями.

Процесс несложный и довольно понятный.
Остался последний штрих.

Создаём контейнер

Недавно на смену NSPersistentStore и NSPersistentStoreCoordinator пришёл более удобный и понятный NSPersistentContainer (собственно, только с ним я и умею работать. Не застал те ужасные времена).

В стандартной работе с coreData через графический интерфейс при создании контейнера мы передаём ему лишь имя модели.
В этом случае контейнер разбирает файл описания модели в составе приложения, подгружает (или создаёт) базу данных в соответствии с моделью и позволяет нам с ней работать.
Но у persistentContainer есть и другой инициализатор.

lazy var persistentContainer: NSPersistentContainer { let _container = NSPersistentContainer(name: "CyrmaxModel", managedObjectModel: model) _container.loadPersistentStores { (description, error) in // Если нужно, делаем что-то } return _container }

Как видите, мы передаём инициализатору контейнера не только имя будущей (или существующей) базы данных, но также передаём ранее созданную модель данных.

После создания persistentContainer мы можем работать с coreData абсолютно так же, как работали до переноса модели в код.

Плюсы описания модели в коде

  1. При запуске приложения не может возникнуть ошибка чтения файла модели, так как файла изначально просто нет;
  2. Приложение запускается чуть быстрее (опять-таки из-за того, что не нужно считывать файл);
  3. Мы автоматически избавляемся от ненужных опционалов в субклассах (это можно сделать и вручную, сгенерировав субклассы и удалив лишние "?", но всё же).

Сокращаем количество кода

С добавленными нами инициализаторами всё выглядит довольно симпатично, но представим ситуацию, когда у нашей сущности не два свойства, а, допустим, десять или больше.
Тогда описание модели превращается в длинную простыню похожего кода, который неудобно читать.
Давайте упакуем этот код внутрь класса сущности, чтобы каждый класс описывал все свои свойства внутри себя.
Рассмотрим на примере класса WorkerMO, так как у него больше свойств.

@objc(WorkerMO) public class WorkerMO: NSManagedObject { @NSManaged public var firstname: String @NSManaged public var lastname: String  static private var _entityDescription: NSEntityDescription? static func entityDescription() -> NSEntityDescription { guard self._entityDescription == nil else { return _entityDescription! } let des = NSEntityDescription(from: self) des.addProperty(NSAttributeDescription(name: "firstname", ofType: .stringAttributeType)) des.addProperty(NSAttributeDescription(name: "lastname", ofType: .stringAttributeType)) self._entityDescription = des return self._entityDescription! } }

То же самое делаем с CompanyMO (но с этим вы и сами справитесь).

А теперь разбор полётов.
Зачем мы добавили статичное свойство _entityDescription и возвращаем именно его значение, а не создаём каждый раз новое описание сущности?
Об этом мы поговорим во второй части статьи, так как в первую очередь подобное поведение связано именно с NSRelationshipDescription и его особенностями.
Но в общих словах, нам необходимо, чтобы CompanyMO.entityDescription() возвращала всегда один и тот же экземпляр NSEntityDescription, а не создавала новый.

Заключение

Создание coreData-модели из кода — сложный процесс со своими нюансами и подводными камнями. В большинстве случаев программисту это не нужно, проще воспользоваться функционалом графического редактора модели. Но модель в коде ускоряет сборку (не так сильно, как избавление от StoryBoard, но всё же ощутимо), уменьшает время на запуск приложения у пользователя и избавляет нас от необходимости обрабатывать ошибки при чтении файла модели с диска.
Ну а я пошёл на подобное усложнение исключительно из-за того, что я незрячий, и мне вслепую очень трудно работать как со StoryBoard, так и с XCDataModel.

Очень рекомендую изучить

  1. NSManagedObjectModel;
  2. NSEntityDescription;
  3. NSPropertyDescription и NSAttributeDescription;
  4. Необязательно, но можно ещё почитать про NSPersistentContainer и его init(name:managedObjectModel:) инициализатор.

ссылка на оригинал статьи https://habr.com/ru/post/498708/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *