我正在尝试迁移我的 Core Data 应用程序以使用
SwiftData
。目标是完全取代 Core Data。
在开发新版本时,我还对数据库结构进行了相当大的更改(添加实体、添加属性、重命名属性、添加关系、将可选属性转换为非可选属性……)。因此我必须使用自定义迁移。在 WWDC 演讲之后,我添加了
SchemaMigrationPlan
以及我的 VersionedSchema
’s
由于某种原因,它甚至没有到达我的自定义迁移。我做错了什么?
控制台甚至不打印自定义迁移中的
print()
语句。
@main
struct MyApp: App {
let container: ModelContainer
let dataUrl = URL.applicationSupportDirectory.appending(path: "MyApp.sqlite")
init() {
let finalSchema = Schema([Loan.self, Item.self, Person.self, Location.self])
do {
container = try ModelContainer(
for: finalSchema,
migrationPlan: MyAppMigrationPlan.self,
configurations: ModelConfiguration(url: dataUrl))
} catch {
fatalError("Failed to initialize model container.")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
PS:我需要为我的 Core Data 文件指向一个自定义 url,因为几年前我重命名了我的应用程序,并且当时没有更改
NSPersistentContainer
的名称。
enum MyAppMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2Step1.self, SchemaV2Step2.self]
}
// Migration Stages
static var stages: [MigrationStage] {
[migrateV1toV2Step1, migrateV1toV2Step2]
}
// v1 to v2 Migrations
static let migrateV1toV2Step1 = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2Step1.self,
willMigrate: nil,
didMigrate: { context in
print("Start migrating Core Data to SwiftData") // This isn’t printed in the Xcode debug console!?
let loans = try? context.fetch(FetchDescriptor<SchemaV2Step1.Loan>())
loans?.forEach { loan in
// Mapping old optional attribute to new non-optional attribute
loan.amount = Decimal(loan.price ?? 0.0)
// doing other stuff
}
// Save all changes
try? context.save()
})
// Adding new attributes and deleting old attributes replaced in step 1.
static let migrateV1toV2Step2 = MigrationStage.lightweight(
fromVersion: SchemaV2Step1.self,
toVersion: SchemaV2Step2.self)
}
此架构反映了我旧的核心数据堆栈,是使用 Xcode 的“创建 SwiftData 代码”操作创建的。由于某种原因,Xcode 生成的代码将
price
属性更改为 Double
。核心数据定义使用 Decimal
强硬。
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Loan.self, Person.self]
}
// definition of all the model classes
@Model
final class Loan {
var price: Double? = 0.0
// …
}
final class Person {
// …
}
}
除此之外,此模式还引入了一个新的非可选属性
amount
,它将取代 price
:
enum SchemaV2Step1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 5, 0)
static var models: [any PersistentModel.Type] {
[Loan.self, Person.self, Location.self]
}
// definition of all the model classes
@Model
final class Loan {
var amount: Decimal = 0.0 // Will replace `price`
var price: Double? = 0.0
// …
}
final class Person {
// …
}
// …
}
最终的模式主要删除了上面提到的旧的替换属性:
enum SchemaV2Step2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Loan.self, Item.self, Person.self, Location.self]
}
// definition of all the model classes
@Model
final class Loan {
var amount: Decimal = 0.0
// …
}
final class Person {
// …
}
// …
}
为了简短起见,我不会显示所有模式的所有详细信息。无论如何,问题似乎出在其他地方......
最初它在迁移过程中崩溃了:
UserInfo={entity=Loan, attribute=amount, reason=Validation error missing attribute values on mandatory destination attribute}
我已经通过为所有非可选值设置默认值来解决这个问题。我认为问题是我的自定义迁移代码仍然没有执行,因此它只是使用所有默认值而不是我的自定义映射。有什么想法吗?
我找到了解决这个问题的方法。 在传递 Schema 参数以使用自定义配置时,它似乎不会读取您的迁移计划。 (我不明白为什么会发生)
不使用自定义配置
ModelContainer
可以很好地进行迁移。
相反,我们可以将此初始化器用于
ModelContainer
。
public convenience init(for forTypes: PersistentModel.Type..., migrationPlan: (SchemaMigrationPlan.Type)? = nil, configurations: ModelConfiguration...) throws
示例代码
let databasePath = URL.documentsDirectory.appending(path: "database.store")
let configuration = ModelConfiguration(url: databasePath)
let container = try ModelContainer.init(
for: ActiveScheme.Item.self,
migrationPlan: Plan.self,
configurations: configuration
)
要检查您的迁移是否已正确加载,
print(modelContainer.migrationPlan)
必须打印 NOT nil。