SwiftData嵌套父子对象锁定问题及解决方案咨询
问题背景
JSON数据格式
{ "parents": [ { "id": 1, "name": "Parent #1", "child1": { "id": 1, "name": "Child #1" } }, { "id": 2, "name": "Parent #2", "child1": { "id": 1, "name": "Child #1" } } ] }
SwiftData模型定义
@Model class Parent: Decodable, Identifiable { @Attribute(.unique) var id: Int var name: String @Relationship var child: Child? enum CodingKeys: String, CodingKey { case id case name case child } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int.self, forKey: .id) self.name = try container.decode(String.self, forKey: .name) self.child = try container.decodeIfPresent(Child.self, forKey: .child) } } @Model class Child: Decodable, Identifiable { @Attribute(.unique) var id: Int var name: String enum CodingKeys: String, CodingKey { case id case name } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int.self, forKey: .id) self.name = try container.decode(String.self, forKey: .name) } }
数据处理代码
private func handleData(filePath: URL ) { do { let data = try Data(contentsOf: filePath) let decoder = JSONDecoder() let jsonData = try decoder.decode(Parent.self, from: data) jsonData.records.forEach { record in context.insert(record) } // 当多个父对象共享同一子对象时,此处会抛出错误 context.save() } catch { print("ERROR!") } }
核心问题
多个Parent共享同一Child时,批量保存会触发锁定/唯一约束冲突错误,需要解决:
- 更优的批量保存方案
- SwiftData中合并冲突的处理方式
- 解码阶段是否应该查询
ChildID的存在性及实现逻辑
解决方案
1. 错误根源与核心思路
当前解码逻辑会为每个Parent的child字段创建全新的Child实例,即使它们ID相同。插入上下文时,多个同ID的Child违反@Attribute(.unique)约束,导致保存失败。
核心解决思路:先查询数据库中是否存在对应ID的Child,存在则复用实例,不存在再创建新实例。
2. 优化解码与持久化流程(不建议在解码器中操作上下文)
解码器的职责是JSON转对象,直接依赖SwiftData上下文会违反单一职责原则,且可能引发线程问题。建议拆分步骤:
步骤1:定义DTO(数据传输对象)与JSON结构对齐
先创建非SwiftData的结构体,专门用于解码JSON:
struct ParentDTO: Decodable { let id: Int let name: String let child1: ChildDTO? } struct ChildDTO: Decodable { let id: Int let name: String } struct RootDTO: Decodable { let parents: [ParentDTO] }
步骤2:处理DTO,复用或创建SwiftData实例
在数据处理方法中,先统一处理Child的唯一性,再关联Parent:
private func handleData(filePath: URL, context: ModelContext) { do { let data = try Data(contentsOf: filePath) let decoder = JSONDecoder() let rootDTO = try decoder.decode(RootDTO.self, from: data) // 批量查询已存在的Child,构建ID到实例的映射 let childIDs = rootDTO.parents.compactMap { $0.child1?.id } let existingChildren = try context.fetch(FetchDescriptor<Child>(predicate: #Predicate { childIDs.contains($0.id) })) var childMap: [Int: Child] = Dictionary(uniqueKeysWithValues: existingChildren.map { ($0.id, $0) }) // 遍历处理Parent,关联Child实例 for parentDTO in rootDTO.parents { // 检查Parent是否已存在,避免重复插入 let existingParent = try context.fetch(FetchDescriptor<Parent>(predicate: #Predicate { $0.id == parentDTO.id })).first let parent: Parent if let existingParent { // 更新已有Parent的字段 existingParent.name = parentDTO.name parent = existingParent } else { parent = Parent(id: parentDTO.id, name: parentDTO.name) context.insert(parent) } // 关联Child if let childDTO = parentDTO.child1 { if let existingChild = childMap[childDTO.id] { parent.child = existingChild } else { let newChild = Child(id: childDTO.id, name: childDTO.name) context.insert(newChild) parent.child = newChild childMap[childDTO.id] = newChild } } else { parent.child = nil } } // 保存上下文 try context.save() } catch { print("保存失败:\(error)") // 保存失败时回滚上下文 context.rollback() } }
3. SwiftData中的合并冲突处理
SwiftData支持通过conflictHandler参数自定义冲突解决逻辑,在保存时指定:
try context.save(conflictHandler: { conflicts in for conflict in conflicts { // 示例1:保留数据库中的现有数据 conflict.resolution = .keepExisting // 示例2:使用新数据覆盖现有数据 // conflict.resolution = .keepNew // 示例3:手动合并字段 // if let newParent = conflict.newValue as? Parent, let oldParent = conflict.existingValue as? Parent { // oldParent.name = newParent.name // conflict.resolution = .merge // } } })
4. 批量保存优化建议
- 先处理关联对象的唯一性,再统一插入/更新,减少上下文状态变更次数
- 大量数据场景下分批次处理,避免单次操作过多对象引发性能问题
- 确保在主线程操作
ModelContext(SwiftData上下文默认绑定创建线程,跨线程需使用ModelContext.shared或新建上下文)
内容的提问来源于stack exchange,提问作者RNorty
相关产品推荐
相关产品推荐

