Пользовательская миграция SwiftData всегда терпит неудачу — возможная ошибка в SwiftDataIOS

Программируем под IOS
Ответить
Anonymous
 Пользовательская миграция SwiftData всегда терпит неудачу — возможная ошибка в SwiftData

Сообщение Anonymous »

Я использую SwiftData в своем приложении SwiftUI для iOS, и мне нужно реализовать MigrationPlan перед запуском, чтобы все прошло гладко, когда мне понадобится внести изменения в сохраняемые данные.
Например, без миграции, если я изменю тип свойства с Bool на [Bool], приложение выйдет из строя при попытке загрузки SwiftData, поскольку сохраненная модель имеет неправильный тип.

Текущая настройка
Текущая @Model, которая довольно проста по стандартам SwiftData, за исключением того, что у нее гораздо больше свойств, чем я включил здесь:

Код: Выделить всё

@Model
final class MyData
{
var property1: Bool = true
var property2: Bool = true
var property3: Int = 1
// And many more properties...

// Default init.
init()
{
// Assign default values to properties.
self.property1 = true
self.property2 = true
self.property3 = 1
// ...
}

// Dynamic init.
init(
property1: Bool,
property2: Bool,
property3: Int
)
{
self.property1 = property1
self.property2 = property2
self.property3 = property3
// ...
}
}
Создание ModelContainer:

Код: Выделить всё

var sharedModelContainer: ModelContainer =
{
let schema = Schema(
[
MyData.self,
])

let modelConfiguration = ModelConfiguration(
schema:                 schema,
isStoredInMemoryOnly:   false
)

do
{
return try ModelContainer(
for:                schema,
configurations:     [modelConfiguration]
)
}
catch
{
fatalError("Could not create ModelContainer: \(error)")
}
}()

@main
struct myApp: App
{
var body: some Scene
{
WindowGroup
{
ContentView()
}
.modelContainer(sharedModelContainer)
}
}
Попытка миграции
Чтобы начать реализацию миграции, я переименовал модель, чтобы отразить ее версию:

Код: Выделить всё

@Model
final class MyDataV1
{
// No other changes compared to the code shared above.
}
Затем я определил MyDataV2 для представления нового формата данных. Единственное, что изменилось, — это тип свойства1:

Код: Выделить всё

@Model
final class MyDataV2
{
// Changed to an array.
var property1: [Bool] = [true]
var property2: Bool = true
var property3: Int = 1
// And many more properties...

// Migration init.
init(
myDataV1: MyDataV1
)
{
// Convert to an array.
self.property1 = [myDataV1.property1]
self.property2 = myDataV1.property2
self.property3 = myDataV1.property3
// ...
}

// Default init.
init()
{
// Assign default values to properties.
self.property1 = [true]
self.property2 = true
self.property3 = 1
// ...
}

// Dynamic init.
init(
property1: [Bool],
property2: Bool,
property3: Int
)
{
self.property1 = property1
self.property2 = property2
self.property3 = property3
// ...
}
}
Я определил псевдоним типа, отражающий текущую версию, поэтому другим частям моего приложения не нужно постоянно обновлять свои ссылки:

Код: Выделить всё

typealias MyData = MyDataV2
Я определил два экземпляра VersionedSchema: один для представления V1, а другой для представления V2:

Код: Выделить всё

enum MyDataSchemaV1: VersionedSchema
{
static var versionIdentifier: Schema.Version = Schema.Version(1, 0, 0)

static var models: [any PersistentModel.Type]
{
[MyDataV1.self]
}
}

enum MyDataSchemaV2: VersionedSchema
{
static var versionIdentifier: Schema.Version = Schema.Version(2, 0, 0)

static var models: [any PersistentModel.Type]
{
[MyDataV2.self]
}
}
Я определил SchemaMigrationPlan для управления миграцией из MyDataV1 в MyDataV2:

Код: Выделить всё

enum MyDataMigrationPlan: SchemaMigrationPlan
{
static var schemas: [any VersionedSchema.Type]
{
[MyDataSchemaV1.self, MyDataSchemaV2.self]
}

static var stages: [MigrationStage]
{
[migrateFromV1ToV2]
}

private static var migrationFromV1ToV2Data: [MyDataV1] = []

static let migrateFromV1ToV2 = MigrationStage.custom(
fromVersion:    MyDataSchemaV1.self,
toVersion:      MyDataSchemaV2.self,
willMigrate:
{
modelContext in

let descriptor  : FetchDescriptor     = FetchDescriptor()
let v1Data      : [MyDataV1]                   = try modelContext.fetch(descriptor)

migrationFromV1ToV2Data = v1Data

try modelContext.delete(model: MyDataV1.self)
try modelContext.save()
},
didMigrate:
{
modelContext in

migrationFromV1ToV2Data.forEach
{
myDataV1 in

let myDataV2 = MyDataV2(myDataV1: myDataV1)
modelContext.insert(myDataV2)
}

try modelContext.save()
}
)
}
И, наконец, я обновил инициализацию ModelContainer, включив в нее план миграции:

Код: Выделить всё

var sharedModelContainer: ModelContainer =
{
let schema = Schema(
[
MyData.self,
])

let modelConfiguration = ModelConfiguration(
schema:                 schema,
isStoredInMemoryOnly:   false
)

do
{
return try ModelContainer(
for:                schema,
migrationPlan:      MyDataMigrationPlan.self,
configurations:     [modelConfiguration]
)
}
catch
{
fatalError("Could not create ModelContainer: \(error)")
}
}()
Ошибки миграции
Я создал ситуацию, в которой экземпляр MyDataV1 уже был сохранен в SwiftData, затем я реализовал указанные выше изменения и запустил новую версию.
Я добавил журналы в MigrationPlan и подтвердил, что willMigrate запускается и завершается без каких-либо ошибок, но приведенные ниже ошибки появляются еще до запуска DidMigrate и приводят к сбою приложения:

Код: Выделить всё

CoreData: error: Error: Persistent History (3178) has to be truncated due to the following entities being removed: (
MyDataV1
)
CoreData: warning: Warning: Dropping Indexes for Persistent History
CoreData: warning: Warning: Dropping Transactions prior to 3178 for Persistent History
CoreData: warning: Warning: Dropping Changes prior to TransactionID 3178 for Persistent History
CoreData: error: addPersistentStoreWithType:configuration:URL:options:error: returned error NSCocoaErrorDomain (134060)
CoreData: error: userInfo:
CoreData: error: NSLocalizedFailureReason : Instances of NSCloudKitMirroringDelegate are not reusable and should have a lifecycle tied to a given instance of NSPersistentStore.
CoreData: error: storeType: SQLite
CoreData: error: configuration: default
Попытка решения 1
Я подумал, что это может быть потому, что в willMigrate я вызывал следующее:

Код: Выделить всё

try modelContext.delete(model: MyDataV1.self)
try modelContext.save()
Я попробовал удалить эти строки, но ошибка все равно возникла. Этот ответ имеет ту же реализацию и предположительно без ошибок, поэтому неудивительно, что это не было причиной проблемы.

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

Код: Выделить всё

enum MyDataSchemaV2: VersionedSchema
{
static var versionIdentifier: Schema.Version = Schema.Version(2, 0, 0)

static var models: [any PersistentModel.Type]
{
[MyDataV1.self, MyDataV2.self]
}
}
Это не вызывает ту же ошибку, но все равно вылетает со следующей ошибкой еще до начала willMigrate:

Код: Выделить всё

CoreData: error: Attempting to retrieve an NSManagedObjectModel version checksum while the model is still editable. This may result in an unstable verison checksum.  Add model to NSPersistentStoreCoordinator and try again.

CoreData: debug: CoreData+CloudKit: -[PFCloudKitOptionsValidator validateOptions:andStoreOptions:error:](36): Validating options:  containerIdentifier:iCloud.com.myapp.production databaseScope:Private ckAssetThresholdBytes: operationMemoryThresholdBytes: useEncryptedStorage:NO useDeviceToDeviceEncryption:NO automaticallyDownloadFileBackedFutures:NO automaticallyScheduleImportAndExportOperations:YES skipCloudKitSetup:NO preserveLegacyRecordMetadataBehavior:NO useDaemon:YES apsConnectionMachServiceName: containerProvider:
 storeMonitorProvider: metricsClient: metadataPurger: scheduler: notificationListener: containerOptions: defaultOperationConfiguration: progressProvider: test_useLegacySavePolicy:YES archivingUtilities: bypassSchedulerActivityForInitialImport:NO bypassDasdRateLimiting:NO activityVouchers:()

storeOptions: {
NSInferMappingModelAutomaticallyOption = 1;
NSMigratePersistentStoresAutomaticallyOption = 1;
NSPersistentCloudKitContainerOptionsKey = "";
NSPersistentHistoryTrackingKey = 1;
NSPersistentStoreMirroringOptionsKey =     {
NSPersistentStoreMirroringDelegateOptionKey = "";
};
NSPersistentStoreRemoteChangeNotificationOptionKey = 1;
NSPersistentStoreStagedMigrationManagerOptionKey = "";
}

CoreData: error: addPersistentStoreWithType:configuration:URL:options:error: returned error NSCocoaErrorDomain (134504)
CoreData: error: userInfo:
CoreData: error:    NSLocalizedDescription : Cannot use staged migration with an unknown coordinator model version.
CoreData: error: storeType: SQLite
CoreData: error: configuration: default
Я думаю, что эта строка является соответствующей ошибкой:

Код: Выделить всё

Cannot use staged migration with an unknown coordinator model version.
Этот ответ предполагает, что проблема в том, что SwiftData не знает, с какой модели начать миграцию. Я не знаю, как это могло быть, и если это так, то как я могу это исправить.

Попытка решения 3
Затем я попробовал инициализировать ModelContainer, задав ему обе версии, а не только самую последнюю:

Код: Выделить всё

var sharedModelContainer: ModelContainer =
{
let schema = Schema(
[
MyDataV1.self,
MyDataV2.self
])

let modelConfiguration = ModelConfiguration(
schema:                 schema,
isStoredInMemoryOnly:   false
)

do
{
return try ModelContainer(
for:                schema,
migrationPlan:      MyDataMigrationPlan.self,
configurations:     [modelConfiguration]
)
}
catch
{
fatalError("Could not create ModelContainer: \(error)")
}
}()
По-прежнему исключая эти две строки из willMigrate:

Код: Выделить всё

try modelContext.delete(model: MyDataV1.self)
try modelContext.save()
На этот раз программа willMigrate завершилась (как и в самой ранней попытке), но все равно аварийно завершилась с новой ошибкой сразу после завершения willMigrate:

Код: Выделить всё

CoreData: CloudKit: CoreData+CloudKit: -[PFCloudKitStoreMonitor pfcloudstoremonitor_is_holding_your_store_open_waiting_for_cloudkit_activity_to_finish](125): 
: Exporter / importer finished after 1 tries.  Allowing store to deallocate.

CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _performSetupRequest:]_block_invoke(1240): : Failed to set up CloudKit integration for store: 
Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=The mirroring delegate could not initialize because it's store was removed from the coordinator.}

CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate recoverFromError:](2310):  - Attempting recovery from error: Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=The mirroring delegate could not initialize because it's store was removed from the coordinator.}

CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate recoverFromError:]_block_invoke(2329): The store was removed before the mirroring delegate could recover from an error:
Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=The mirroring delegate could not initialize because it's store was removed from the coordinator.}
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _finishedRequest:withResult:](3582): Finished request:  9C16248F-EDFB-46BB-ACEF-EAAC55C1CBBA with result:  storeIdentifier: 2FE6E954-8AAF-4D89-8C6C-741C890ADEFC success: 0 madeChanges: 0 error: Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=The mirroring delegate could not initialize because it's store was removed from the coordinator.}
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate checkAndExecuteNextRequest](3551): : Checking for pending requests.

CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _performSetupRequest:]_block_invoke(1302): Failed to finish setup event: Error Domain=NSCocoaErrorDomain Code=134407 "Request '9C16248F-EDFB-46BB-ACEF-EAAC55C1CBBA' was cancelled because the store was removed from the coordinator." UserInfo={NSLocalizedFailureReason=Request '9C16248F-EDFB-46BB-ACEF-EAAC55C1CBBA' was cancelled because the store was removed from the coordinator.}
CoreData: debug: CoreData+CloudKit: -[NSCloudKitMirroringDelegate observeChangesForStore:inPersistentStoreCoordinator:](427): : Observing store: 
BUG IN CLIENT OF CLOUDKIT: Registering a handler for a CKScheduler activity identifier that has already been registered (com.apple.coredata.cloudkit.activity.export.2FE6E954-8AAF-4D89-8C6C-741C890ADEFC).
В чем может быть соответствующая ошибка:

Код: Выделить всё

Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=The mirroring delegate could not initialize because it's store was removed from the coordinator.}
И около 15 подобных строк было записано во время willMigrate:

Код: Выделить всё

CoreData: debug: CoreData+CloudKit: -[PFCloudKitMetadataModelMigrator calculateMigrationStepsWithConnection:error:](450): Skipping migration for 'ANSCKDATABASEMETADATA' because it already has a column named 'ZLASTFETCHDATE'
Это ошибка CoreData/SwiftData?
Инженер Apple DTS около 3 недель назад предположил, что именно эти ошибки, которые я получаю в разделе Попытка решения 3, являются ошибками платформы.
Итак, что это значит? Миграция SwiftData просто не работает вообще, если вы также включили синхронизацию CloudKit?
Как мне заставить это работать?
https://developer.apple.com/documentati ... onedschema
https://developer.apple.com/documentati ... rationplan

Обновление
Это ошибка SwiftData. Миграция не работает должным образом.
Если вы читаете это, первое, что вам следует сделать, — это отправить отзыв в Apple. Только Apple может это исправить.
Кроме этого, рассмотрите возможность организации своих моделей SwiftData таким образом, чтобы не требовалось никаких миграций, особенно пользовательских.
Облегченные миграции могут обрабатывать большое количество сценариев, но даже они подвержены ошибкам. Я видел несколько учетных записей облегченных миграций с ошибками, в которых в противном случае автоматическая миграция была бы успешной. Автоматическая миграция полностью выполняется SwiftData, при этом разработчик явно не указывает миграцию, но в противном случае она выполнялась бы с помощью облегченной миграции.
Вы также можете вернуться к использованию CoreData.

Подробнее здесь: https://stackoverflow.com/questions/795 ... -swiftdata
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «IOS»