SwiftData迁移报错:必填关联关系缺少属性值
SwiftData自定义Schema迁移:解决必填关联属性缺失错误
问题背景
我正在Swift中使用自定义Schema迁移计划进行数据模型迁移,将设置从SchemaV1迁移至SchemaV2。两个版本均包含FSViz类,但属性有所不同:SchemaV2中用TextSetting类型的textSettings属性替代了SchemaV1的textColor属性。
相关代码实现
模型定义
extension SchemaV1 { @Model public final class FSViz { @Attribute(.unique) public let id: UUID public var textColor: TextColor init(textColor: TextColor) { self.id = UUID() self.textColor = textColor } } } extension SchemaV2 { @Model public final class FSViz { @Attribute(.unique) public let id: UUID public var textSettings: TextSetting init(textSettings: TextSetting) { self.id = UUID() self.textSettings = textSettings } } }
初始化工具类
public struct InitMyApp { static func makeInitialFSViz() -> FSViz? { FSViz(textSettings: makeTextSettings()) } public static func makeFSViz() -> FSViz { FSViz(textSettings: makeTextSettings()) } static func makeTextSettings() -> TextSetting { TextSetting( textColor: TextColor(red: 1, green: 1, blue: 1, alpha: 1) ) } }
迁移计划
enum MyAppMigrationPlan: SchemaMigrationPlan { static var schemas: [VersionedSchema.Type] { [SchemaV1.self, SchemaV2.self] } static var stages: [MigrationStage] { [migrateV1toV2] } static let migrateV1toV2 = MigrationStage.custom(fromVersion: SchemaV1.self, toVersion: SchemaV2.self, willMigrate: { context in let fetchDescriptor = FetchDescriptor<SchemaV1.FSViz>() let allV1Vizs = try context.fetch(fetchDescriptor) for v1Viz in allV1Vizs { let newTextSetting = SchemaV2.TextSetting(textColor: v1Viz.textColor) let newViz = SchemaV2.FSViz(textSettings: newTextSetting) print("migration processing") context.insert(newViz) try context.save() } }, didMigrate: nil) }
容器初始化
public typealias FSViz = SchemaV2.FSViz public typealias TextSetting = SchemaV2.TextSetting @MainActor public let MyAppContainer: ModelContainer = { do { let schema = Schema([FSVis.self, TextSetting.self]) // 疑似笔误:应为FSViz.self let configuration = ModelConfiguration() let container = try ModelContainer(for: schema, migrationPlan: MyAppMigrationPlan.self) let context = container.mainContext if try context.fetch(FetchDescriptor<FSViz>()).isEmpty { if let fsViz = InitWaveBar.makeInitialFSViz() { // 疑似笔误:应为InitMyApp.makeInitialFSViz() container.mainContext.insert(fsViz) } else { print("Error: makeInitialFSViz() returned nil") } } return container } catch { fatalError(error.localizedDescription) } }()
迁移错误信息
Error Domain=NSCocoaErrorDomain Code=134110 "An error occurred during persistent store migration." UserInfo={entity=FSViz, attribute=textSettings, reason=Validation error missing attribute values on mandatory destination relationship}
该错误表明目标Schema中必填的textSettings关联关系缺少属性值。已检查迁移逻辑确认textSettings已正确初始化赋值,但错误仍存在。
问题
- 如何解决迁移过程中出现的“必填关联关系缺少属性值”错误?
- 是否忽略了Swift中迁移时处理关联关系的特定要点?
排查与解决思路
1. 修正迁移上下文的事务处理逻辑
不要在循环内调用try context.save():自定义迁移的willMigrate闭包由SwiftData自动管理事务,手动调用save()会打断迁移流程,导致关联数据未完全绑定就被提交。移除循环内的save(),依赖框架自动处理事务。
修改后的迁移代码:
static let migrateV1toV2 = MigrationStage.custom(fromVersion: SchemaV1.self, toVersion: SchemaV2.self, willMigrate: { context in let fetchDescriptor = FetchDescriptor<SchemaV1.FSViz>() let allV1Vizs = try context.fetch(fetchDescriptor) for v1Viz in allV1Vizs { let newTextSetting = SchemaV2.TextSetting(textColor: v1Viz.textColor) let newViz = SchemaV2.FSViz(textSettings: newTextSetting) print("migration processing") context.insert(newViz) } // 移除循环内的save(),框架自动处理事务提交 }, didMigrate: nil)
2. 确认关联实体已注册到目标Schema
确保SchemaV2的定义中包含TextSetting类型,否则迁移时无法识别该关联实体,导致textSettings属性无法被正确持久化:
enum SchemaV2: VersionedSchema { static var versionIdentifier: String = "2" static var models: [any PersistentModel.Type] { [FSViz.self, TextSetting.self] // 必须包含TextSetting } }
3. 修复容器初始化中的笔误
容器代码存在两处明显笔误,会导致Schema不完整或初始化失败:
- 将
FSVis.self修正为FSViz.self - 将
InitWaveBar.makeInitialFSViz()修正为InitMyApp.makeInitialFSViz()
4. 验证关联属性的可选性(临时规避方案)
如果业务逻辑允许,可将SchemaV2.FSViz的textSettings声明为可选类型public var textSettings: TextSetting?,但这仅为临时规避,建议确保迁移时必选属性均被正确赋值。
5. 新增迁移后验证逻辑
在didMigrate闭包中检查迁移后的数据完整性,排查是否有数据未被正确迁移:
static let migrateV1toV2 = MigrationStage.custom(fromVersion: SchemaV1.self, toVersion: SchemaV2.self, willMigrate: { context in // ... 迁移逻辑 }, didMigrate: { context in let migratedVizs = try context.fetch(FetchDescriptor<SchemaV2.FSViz>()) for viz in migratedVizs { guard viz.textSettings != nil else { print("迁移后FSViz \(viz.id)缺少textSettings") continue } } })
内容的提问来源于stack exchange,提问作者Hajime
相关产品推荐
相关产品推荐

