Site icon AppTractor

Что такое Core Data и как с ней работать

Этот пост предназначен для новичков в разработке на платформах Apple. Поскольку Core Data, несомненно, кажется сложной для многих новых разработчиков, я решил попытаться объяснить работу простыми словами.

Если вы посмотрите в Интернете определение, вы в Apple Developer сможете найти что-то вроде такого:

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

Или может быть что-то вроде этого в Wiki:

Core Data — это граф объектов и фреймворк обеспечения персистентности, предоставляемый Apple в операционных системах macOS и iOS.

Это мало что объясняет.

Прежде чем мы перейдем к простому объяснению того, что такое Core Data, давайте начнем с вопроса «зачем».

Зачем нам нужно что-то вроде Core Data?

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

Хотя вы можете хранить данные для основного приложения для заметок в виде простого файла с помощью Codable, этот подход плохо масштабируется. Для каждого небольшого изменения вам нужно снова перезаписывать весь файл. Если вы позже решите изменить свою модель данных (в данном случае вашу структуру/класс Codable), вам нужно быть очень осторожным, иначе ваши пользователи могут легко потерять данные, так как их файл структурирован в соответствии со старым определением, а вы его изменили. Как вы, вероятно, знаете, UserDefaults не следует использовать для хранения больших объемов данных, а скорее для настроек пользователя.

Чтобы хранить и извлекать большие объемы данных, вам нужна база. Обычно стандартные базы данных требуют своего собственного языка (SQL) для извлечения и сохранения данных. Если вы хотите напрямую использовать базу данных из вашего кода Swift, вам нужно будет вручную создать команды SQL, выполнять их, а затем вручную анализировать результат.

К счастью, у нас есть Core Data, которая обрабатывает все это за нас.

Что такое Core Data на самом деле?

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

«Граф объектов» и другие причудливые описания означают то, что Core Data может интеллектуально контролировать ваши классы, отслеживать изменения, и когда вы вызываете save в контексте объекта, эти изменения сохраняются в базе данных. Фреймворк также управляет  миграцией, что довольно часто встречается в мире баз данных SQL. Поскольку эти базы данных имеют строго определенную структуру, в соответствии с которой они сохраняют данные, если вы решите изменить свою модель (это означает такие вещи, как добавление/удаление свойств в ваших классах, переименование свойств или добавление отношений), вам необходимо выполнить миграцию. Это процесс, который обеспечивает актуальность структуры базы данных (называемой «схемой») и ее готовность к работе с новыми определениями модели. Это означает, что если у вас есть имя свойства, то в базе данных есть столбец для сохранения этой информации.

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

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

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

Части Core Data

В этом разделе мы рассмотрим основными частями Core Data, с которыми вы столкнетесь, когда начнете их использовать.

Я лично не являюсь фанатом стандартной настройки Core Data, которую вы получаете, если установите флажок при создании проекта в Xcode. Это похоже на волшебство, и что-то, что только Xcode может добавить в ваш проект… В любом случае, это тоже тема для другой статьи.

.xcdatamodeld

Это файл модели Core Data, обычно называемый как-то вроде «Модель», «База данных» или как текущий проект. По сути, это шаблон для Core Data, который сообщает ему, какие объекты мы планируем хранить, каковы их свойства и отношения между ними.

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

В этом файле нет ничего загадочного, это просто файл XML (XML — это стандартизированный формат для обмена данными, что означает, что различные языки программирования знают, как его открыть). Фактически, мы можем просмотреть  этот файл и увидеть внутреннее содержимое.

Xcode не позволит вам увидеть исходный файл, но вы можете найти его на диске и открыть в любом текстовом редакторе. Если вы выберете тот, который поддерживает синтаксис XML, его будет легче читать.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17511" systemVersion="19H2" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
    <entity name="Joke" representedClassName="Joke" syncable="YES">
        <attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
        <attribute name="punchline" attributeType="String"/>
        <attribute name="setup" attributeType="String"/>
    </entity>
    <elements>
        <element name="Joke" positionX="-63" positionY="-18" width="128" height="88"/>
    </elements>
</model>

Это содержимое — модель данных, которая определяет сущность Joke. В отличие от таких файлов, как .storyboard или .xib, эти файлы довольно хорошо читаются.

NSManagedObject

Все классы Core Data являются подклассами этого класса, что позволяет им работать с Core Data. Это стандартные классы Swift с несколькими дополнительными аннотациями. Выше мы видели сущность Joke, представленную в модели, а здесь она представлена как класс:

import CoreData
class Joke: NSManagedObject {
    @NSManaged var id: Int
    @NSManaged var setup: String
    @NSManaged var punchline: String
    @NSManaged var created: Date
}

@NSManaged — это специальная аннотация, которая позволяет Core Data работать с этими свойствами особым образом. Это не обертка свойств, хотя выглядит так же. Благодаря атрибуту Core Data может отслеживать изменения свойств объекта и, следовательно, знать, что необходимо сохранить.

Это также позволяет Core Data заполнять эти свойства по мере необходимости, они могут быть пустыми, без нашего ведома или заботы об этом. Сущности Core Data часто возвращаются как “faults”, что означает, что это пустые объекты и данные заполнятся тогда, когда ваше приложение запросит их. Это отличная оптимизация. Если вы получаете 1000 объектов из Core Data, а ваша коллекция отображает только 20 из них без пользовательской прокрутки, для остальных объектов данные не обязательно должны присутствовать в памяти.

NSPersistentContainer

Это «основной» класс, охватывающий работу с Core Data. В его обязанности входит загрузка модели данных (файл .xcdatamodeld) и, возможно, реагирование, если он не может ее найти или отсутствуют классы для определенных сущностей.

Обычно вы инициируете его с именем файла модели, а затем вызываете loadPersistentStores для загрузки.

Он также имеет очень важное свойство viewContext, которым является NSManagedObjectContext, и мы рассмотрим его дальше.

NSManagedObjectContext

Этот класс позволяет нам получать данные из базы данных, а также сохранять их. Вы получаете данные вызывая fetch в этом контексте, для чего требуется экземпляр NSFetchRequest. Ваше приложение обычно имеет один основной контекст (которого может хватить во многих случаях). Этот контекст имеет связанные сущности, поэтому он может отслеживать изменения и сохранять их при необходимости.

Это также причина, по которой вам нужно передать контекст в инициализацию NSManagedObject. Таким образом вы связываете объект с контекстом, который будет им управлять. Помимо уже упомянутой выборки, вы, вероятно, будете часто использовать метод сохранения, а также есть свойство hasChanges для проверки перед сохранением.

Прежде чем мы продолжим, нам нужно вернуться к NSPersistentContainer. Мы можем вызвать для него performBackgroundTask, что даст нам замыкание с фоновым NSManagedObjectContext в качестве входного параметра. Таким образом, мы можем довольно легко выполнить фоновую работу с базой данных, не влияя на видимую производительность. Вы можете использовать выборку в этом фоновом контексте, изменять сущности и затем сохраняя их.

NSFetchRequest

Экземпляр NSFetchRequest сообщает NSManagedObjectContext, что вы хотите и каким образом. Это общий для NSManagedObject, который позволяет нам указать, какой тип сущности мы хотим получить.

Затем вы можете настроить фильтр (используя NSPredicate), а также сортировку (используя массив экземпляров NSSortDescriptor). В базовых случаях нужна только сортировка, потому что вы, вероятно, хотите показать пользователю все данные.

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

NSPredicate

Это «метод фильтрации» в мире Core Data. У него есть слабое место, потому что вам нужно указать его как строку специального формата, и если вы сделаете ошибку, вы узнаете об этом только при запуске приложения.

Вот очень простой пример:

NSPredicate(format: "%K == %@", #keyPath(Note.wasDeleted), NSNumber(value: false))

Здесь используется функция Swift #keyPath, по крайней мере, для обеспечения некоторой безопасности. Давайте более внимательно посмотрим на короткую строку формата: %K == %@. Эти значения в процентах отмечают части строки, которые должны быть заменены значением. В данном случае %K — зарезервировано для путей, а %@ — для объектов. Значение внутри %@ будет заключено в кавычки. NSNumber — это пережиток ObjC и своего рода приемлемый способ работы со значениями bool в NSPredicate.

Приведенный выше предикат будет фильтровать сущности Note, для которых свойство wasDeleted имеет значение false. Пометка элементов как удаленных вместо их удаления называется «мягким удалением» и очень полезна. Вы можете легко реализовать что-то вроде функции «Корзина», а также, если вы обрабатываете облачную синхронизацию, это, по сути, необходимо для отслеживания удаленных элементов.

Мы могли бы переписать приведенный выше предикат по-разному:

NSPredicate(format: "wasDeleted == %@"), NSNumber(value: false))

Это короче, но если мы переименуем wasDeleted в будущем, это перестанет работать.

Другой, еще более короткий вариант:

NSPredicate(format: "wasDeleted == NO"))

Для этого необходимо знать, что false соответствует «NO», а true — «YES». Я думаю, что первый наиболее подробный вариант — самый безопасный и сломается он с меньшей вероятностью.

NSSortDescriptor

С экземпляром этого класса мы можем сообщить NSFetchRequest, как сортировать наши сущности. Вы можете предоставить массив, и порядок будет определять, как элементы будут отсортированы по нескольким свойствам.

Инициализация ожидает, что ключ и логическое значение указывают, будет ли сортировка по возрастанию или по убыванию (12345 против 54321). Подобно NSPredicate, я настоятельно рекомендую использовать #keyPath, чтобы избежать потенциальных проблем при переименовании атрибутов.

NSSortDescriptor(key: #keyPath(Note.title), ascending: true)

Это базовый пример, который сортирует объекты Note по их заголовку. Если бы мы хотели сначала показать избранные заметки, мы бы передали этот массив в NSFetchRequest

[
    NSSortDescriptor(key: #keyPath(Note.isFavorite), ascending: true),
    NSSortDescriptor(key: #keyPath(Note.title), ascending: true)
]

Сначала мы получали все избранные заметки, отсортированные по заголовку, а затем все остальные, также отсортированные по заголовку.

NSFetchedResultsController

Обычно сокращаемый до «FRC», этот класс в основном создан для UITableView и UICollectionView. Он управляет извлечением данных из базы данных за вас, а также сообщать вам количество разделов и элементов в определенных разделах, которые необходимы для реализации источников данных.

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

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

Я надеюсь, что этот пост помог вам понять основы Core Data, и если что-то отсутствует или неясно, пожалуйста, дайте мне знать в Twitter, и я сделаю все возможное, чтобы улучшить это.

Спасибо за прочтение. А теперь идите и создайте что-нибудь с Core Data! :-)

Что еще почитать про Core Data:

Exit mobile version