问题描述
最近有个朋友通过邮件请教一个问题,在运行下述代码时:
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var trainTables: [TrainTableModelData]
@Query private var trainRounds: [TrainRoundModelData]
var body: some View {
VStack {
Text("TrainTable Count: \(trainTables.count)")
Text("TrainRound Count: \(trainRounds.count)")
Button("Add TrainTable with Rounds") {
let trainTable = TrainTableModelData(name: "Training Plan C")
modelContext.insert(trainTable)
var tmpTrainRounds = [TrainRoundModelData]()
for i in 1..<10 {
let trainRound = TrainRoundModelData(roundIndex: Int32(i), breathSecond: 40, holdSecond: 70)
tmpTrainRounds.append(trainRound)
trainRound.trainTable = trainTable
}
}
.padding()
Button("Delete TrainTable") {
do {
try modelContext.delete(model: TrainRoundModelData.self)
try modelContext.save()
} catch {
print(error.localizedDescription)
}
}
.padding()
}
.padding()
}
}
当调用modelContext.delete时,Xcode会输出如下报错:
The operation couldn’t be completed. Constraint trigger violation: Batch delete failed due to mandatory OTO nullify inverse on TrainRoundModelData/trainTable

通过查询相关资料,找到《SwiftData: Batch delete failed due to mandatory OTO nullify inverse》这篇文章。这篇文章的答案告知,当delete(model:)用于具有关系的类型时,关系的双方都需要是可选的。
import SwiftData
import SwiftUI
@Model
class TrainTableModelData { // 训练表模型数据
@Attribute(.unique) var id: UUID
@Relationship(deleteRule: .cascade) var trainRounds = [TrainRoundModelData]() // 训练轮次,一对多关系,级联
var name: String
init(id: UUID = UUID(), name: String) {
self.id = id
self.name = name
}
}
@Model
class TrainRoundModelData { // 训练模型数据
@Attribute(.unique) var id: UUID // UUID
@Relationship var trainTable: TrainTableModelData? // 训练表模型数据
var roundIndex: Int32
var breathSecond: Int32
var holdSecond: Int32
init(id: UUID = UUID(), roundIndex: Int32, breathSecond: Int32, holdSecond: Int32, trainTable: TrainTableModelData? = nil) {
self.id = id
self.roundIndex = roundIndex
self.breathSecond = breathSecond
self.holdSecond = holdSecond
self.trainTable = trainTable
}
}
在这段测试代码中,可以看到 TrainTableModelData 类中的 trainRounds 字段并不是可选的,也就导致了运行delete(model:)时发生了报错。
@Relationship(deleteRule: .cascade) var trainRounds = [TrainRoundModelData]() // 训练轮次,一对多关系,级联
解决方案
解决方案为,将 trainRounds 字段改为可选类型:
@Relationship(deleteRule: .cascade) var trainRounds: [TrainRoundModelData]? // 训练轮次,一对多关系,级联

这样,在执行的时候,就不会有报错产生,并且可以通过delete(model:)成功删除数据。
关于报错的原因,教程中已经指出,SwiftData在删除时尝试nullify(设为nil),因为具有关系的类型不是可选字段,导致设为nil的操作失败,因此导致报错。
因此,设置关系类型的字段为可选字段后,删除操作执行成功。
其他解决方案
解决方案1
因为 TrainRoundModelData 依赖 TrainTableModelData,可以先删除 TrainTableModelData,然后 TrainRoundModelData 会被级联删除。
do {
try modelContext.delete(model: TrainTableModelData.self) // 先删除 trainTable
try modelContext.save()
} catch {
print(error.localizedDescription)
}
删除TrainRoundModelData后,与之关联的TrainRoundModelData一并被删除。
解决方案2
如果想先删除所有 TrainRoundModelData,可以手动删除:
do {
let trainRounds = try modelContext.fetch(FetchDescriptor<TrainRoundModelData>())
for trainRound in trainRounds {
modelContext.delete(trainRound)
}
try modelContext.save()
} catch {
print(error.localizedDescription)
}
这种方式不会触发批量删除的约束问题,因为它是逐条删除。
总结
这里实际还是SwiftData的@Relationship的相关报错,这一问题同时出现在SwiftData同步iCloud中。在同步iCloud的SwiftData对象中,字段需要提供默认值,关系则需要为可选类型,否则无法同步到iCloud。
相关文章
1、SwiftData: Batch delete failed due to mandatory OTO nullify inverse:https://stackoverflow.com/questions/77814385/swiftdata-batch-delete-failed-due-to-mandatory-oto-nullify-inverse
2、SwiftData数据同步到iCloud:https://fangjunyu.com/2024/11/10/swiftdata%e6%95%b0%e6%8d%ae%e5%90%8c%e6%ad%a5%e5%88%b0icloud/