Разработка
Используйте транзакции вместо сохранения в 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 имеет две важные особенности:
- Автоматический коммит: Разработчикам не нужно явно вызывать метод сохранения; SwiftData автоматически сохранит данные после выполнения замыкания.
- Немедленное сохранение: Даже если в
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.
Такая реализация дает множество преимуществ:
- Безопасность параллелизма: Обеспечивает потокобезопасность операций с данными с помощью акторов и кастомных исполнителей.
- Понятные интерфейсы: Предоставляет полный набор внутренних и внешних интерфейсов для обработки транзакций.
- Безопасность типов: Правильно обрабатывает требования протокола Sendable, добавляя обработку возвращаемых значений.
- Простота использования: Унифицированный шаблон обработки транзакций упрощает процесс разработки.
Используя эти методы транзакций вместо прямых вызовов save, мы можем лучше контролировать детализацию транзакций, избегать создания слишком большого количества транзакций, тем самым повышая производительность приложения и обеспечивая надежность операций с данными.
-
Аналитика магазинов2 недели назад
Мобильный рынок Ближнего Востока: исследование Bidease и Sensor Tower выявляет драйверы роста
-
Интегрированные среды разработки3 недели назад
Chad: The Brainrot IDE — дикая среда разработки с играми и развлечениями
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.45
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.46

