You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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已正确初始化赋值,但错误仍存在。

问题

  1. 如何解决迁移过程中出现的“必填关联关系缺少属性值”错误?
  2. 是否忽略了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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 04:20:58