Connect with us

Разработка

Используйте транзакции вместо сохранения в SwiftData и Core Data

Фреймворк SwiftData вводит метод transaction в ModelContext, предоставляя разработчикам более элегантный способ организации и управления операциями с данными.

Опубликовано

/

     
     

Обеспечение согласованности и целостности данных имеет решающее значение в операциях сохранения данных. Фреймворк SwiftData вводит метод transaction в ModelContext, предоставляя разработчикам более элегантный способ организации и управления операциями с данными. В этой статье мы рассмотрим, как использовать концепцию транзакций для создания более надежных и эффективных операций сохранения данных.

Понимание транзакций

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

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

Неявный механизм обработки транзакций в Core Data

Core Data не предоставляет явных команд управления транзакциями, таких как BEGIN TRANSACTION или COMMIT; вместо этого он неявно интегрирует концепцию транзакций во фреймворк. При вызове метода save Core Data автоматически упаковывает все изменения в текущем контексте в одну транзакцию и отправляет их в SQLite.

Давайте разберемся в этом механизме на примере кода. Сначала рассмотрим пример, в котором метод save вызывается несколько раз:

let newItem = Item(context: viewContext)
newItem.timestamp = Date()
try? viewContext.save() // First transaction
let newItem1 = Item(context: viewContext)
newItem1.timestamp = Date()
try? viewContext.save() // Second transaction

По другому, мы можем объединить все операции в один вызов save:

let newItem = Item(context: viewContext)
newItem.timestamp = Date()
let newItem1 = Item(context: viewContext)
newItem1.timestamp = Date()
try? viewContext.save() // Single transaction encompassing all operations

Эта разница не только влияет на производительность, но и, что более важно, на надежность операций с данными. Рассмотрим реальный сценарий: создание Topic и добавление в него изображений. Такие составные операции требуют, чтобы все шаги были успешно завершены; в противном случае необходим полный откат. В этом случае использование одной транзакции особенно важно:

do {
    let topic = Topic(context: context)
    let image = Image(context: context)
    image.topic = topic
    try context.save()  // Package all operations into a single transaction
} catch {
    context.rollback()  // Complete rollback in case of an error
}

Операция отката Core Data (rollback) всегда действует на всю транзакцию. Она восстанавливает контекст на момент последнего успешного вызова сохранения, обеспечивая согласованность данных.

Влияние консолидации транзакций на производительность

В Core Data и SwiftData правильное использование транзакций не только обеспечивает согласованность данных, но и может значительно повысить производительность приложения. Давайте посмотрим, как фреймворк обрабатывает транзакции.

Чтобы увидеть конкретные детали того, как Core Data строит транзакции, мы можем включить отладочный вывод в Xcode с помощью опции: -com.apple.CoreData.SQLDebug 1. Вот простая операция вставки данных:

let newItem = Item(context: viewContext)
newItem.timestamp = Date()
try? viewContext.save()

Через вывод отладки мы видим, что Core Data фактически создает две отдельные транзакции для этой простой операции:

// Transaction 1: Assigning primary key
CoreData: sql: BEGIN EXCLUSIVE // Start transaction
CoreData: sql: SELECT Z_MAX FROM Z_PRIMARYKEY WHERE Z_ENT = ?
CoreData: annotation: getting max pk for entityID = 1
CoreData: sql: UPDATE OR FAIL Z_PRIMARYKEY SET Z_MAX = ? WHERE Z_ENT = ? AND Z_MAX = ?
CoreData: annotation: updating max pk for entityID = 1 with old = 6 and new = 7
CoreData: sql: pragma auto_vacuum
CoreData: annotation: sql execution time: 0.0000s
CoreData: sql: pragma auto_vacuum=2
CoreData: annotation: sql execution time: 0.0000s
CoreData: sql: COMMIT // Commit transaction

// Transaction 2: Inserting data and updating history tracking
CoreData: sql: BEGIN EXCLUSIVE
CoreData: sql: INSERT INTO ZITEM(Z_PK, Z_ENT, Z_OPT, ZTIMESTAMP) VALUES(?, ?, ?, ?)
CoreData: details: SQLite bind[0] = (int64)7
CoreData: details: SQLite bind[1] = (int64)1
CoreData: details: SQLite bind[2] = (int64)1
CoreData: details: SQLite bind[3] = (timestamp)753434654.313978
... Updating historical data
CoreData: sql: COMMIT

Этот вывод раскрывает важный факт:

  • Дополнительные накладные расходы: Каждый раз при вызове save Core Data или SwiftData приходится выполнять дополнительные операции на уровне фреймворка, что приводит к дополнительным накладным расходам на транзакцию.
  • Триггеры уведомлений: Механизмы реагирования фреймворка на данные (такие как уведомления didSave или постоянное отслеживание истории) также запускаются на основе каждой транзакции. Частые фиксации транзакций не только увеличивают накладные расходы на работу с базой данных, но и приводят к увеличению количества ответов на уведомления, что негативно сказывается на отзывчивости пользовательского интерфейса.

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

Для более глубокого изучения механизма первичного ключа (Z_PK) в Core Data вы можете обратиться к статье «Как Core Data сохраняет данные в SQLite«.

API транзакций SwiftData

SwiftData вводит метод transaction в ModelContext, предоставляя разработчикам более элегантный и явный способ обработки транзакций. Такой дизайн не только упрощает операции с транзакциями, но и направляет разработчиков на принятие «транзакционного» мышления программирования, поощряя упаковку связанной бизнес-логики в полные транзакционные единицы.

public func transaction(block: () throws -> Void) throws

Метод transaction имеет две важные особенности:

  1. Автоматический коммит: Разработчикам не нужно явно вызывать метод сохранения; SwiftData автоматически сохранит данные после выполнения замыкания.
  2. Немедленное сохранение: Даже если в mainContext включена функция autosaveEnabled (автоматическое сохранение), этот метод будет игнорировать эту настройку, обеспечивая сохранение данных сразу после завершения закрытия.

Вот реальный пример использования:

try? modelContext.transaction {
    let item = Item(timestamp: Date())
    modelContext.insert(item)
    let item2 = Item(timestamp: Date())
    modelContext.insert(item2)
}

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

Реализация более комплексной обработки транзакций в ModelActor

Благодаря тому, что SwiftData представила элегантную модель параллельного программирования @ModelActor, мы можем построить более безопасную и эффективную архитектуру операций с данными. В этой архитектуре все операции по изменению данных инкапсулируются внутри актора и выполняются в фоновом контексте, а контекст основного потока отвечает только за получение данных. Поэтому нам также необходимо обеспечить механизм обработки транзакций, основанный на модели акторов.

Пожалуйста, прочитайте книгу «Practical SwiftData: Создание приложений SwiftUI с помощью современных подходов» и «Конкурентное программирование в SwiftData«, чтобы узнать, как использовать @ModelActor.

@ModelActor
public actor DataHandler {}

extension DataHandler {
    func save(_ saveImmediately: Bool) throws {
        if saveImmediately, modelContext.hasChanges {
            try modelContext.save()
        }
    }

    /// Internal transaction method accepting a synchronous closure; return value does not need to conform to Sendable
    func transaction<T>(_ block: () throws -> T) throws -> T {
        let result = try block()
        try save(true)
        return result
    }

    /// Internal transaction method accepting an asynchronous closure; return value needs to conform to Sendable
    func transaction<T: Sendable>(_ block: () async throws -> T) async throws -> T {
        let result = try await block()
        try save(true)
        return result
    }
}

Чтобы облегчить использование за пределами модуля данных, нам также необходимо обеспечить более удобные публичные интерфейсы:

extension DataHandler {
    /// Public transaction method accepting a synchronous closure; return value conforms to Sendable
    /// - Parameter block: Synchronous operation closure accepting a DataHandler instance
    public func transaction<T: Sendable>(_ block: (DataHandler) throws -> T) throws -> T {
        let result = try block(self)
        try save(true)
        return result
    }

    /// Public transaction method accepting an asynchronous closure; return value conforms to Sendable
    /// - Parameter block: Asynchronous operation closure accepting a DataHandler instance
    func transaction<T: Sendable>(_ block: (DataHandler) async throws -> T) async throws -> T {
        let result = try await block(self)
        try save(true)
        return result
    }
}

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

Такая реализация дает множество преимуществ:

  1. Безопасность параллелизма: Обеспечивает потокобезопасность операций с данными с помощью акторов и кастомных исполнителей.
  2. Понятные интерфейсы: Предоставляет полный набор внутренних и внешних интерфейсов для обработки транзакций.
  3. Безопасность типов: Правильно обрабатывает требования протокола Sendable, добавляя обработку возвращаемых значений.
  4. Простота использования: Унифицированный шаблон обработки транзакций упрощает процесс разработки.

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

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.

Наши партнеры:

LEGALBET

Мобильные приложения для ставок на спорт
Хорошие новости

Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: