这一节,基于上一节创建的 Series,Episode 和 History 记录之间的关系,我们来看 Delete Rule 在从 Core Data 中删除数据时,不同的选项对删除结果产生的实际影响。以此,彻底理解这些配置的作用。
在开始之初,core data 里所有的记录关系是这样的:

Cascade
观察 cascade 的删除效果
我们从删除 Series 中的记录开始。按照之前的设计,删除一条 Series 记录,与之对应的所有 Episode 记录也会一并删除。为了删除记录,我们要先获得表示该记录的 Series
对象:
func deleteSeries1() {
/// 1. Create `NSFetchRequest` objects.
let seriesRequest = NSFetchRequest<Series>(entityName: "Series")
do {
/// 2. Fetch records from core data.
let series = try managedContext
.fetch(seriesRequest)
.filter { $0.id == 1 }
.first!
}
catch let error as NSError {
print(error.localizedDescription)
}
}
得到 series
之后,直接把它传给 managedContext.delete
方法就可以删除对应的记录了:
do {
/// 2. Fetch records from core data.
let series = try managedContext
.fetch(seriesRequest)
.filter { $0.id == 1 }
.first!
managedContext.delete(series)
CoreDataManager.shared.saveContext(managedContext)
}
catch let error as NSError {
print(error.localizedDescription)
}
要注意的是,和添加记录时相同,别忘了在 delete
之后向 Core Data 保存 managedContext
中的内容,否则这个删除是不生效的。接下来,重新查询 core data 中 Series 和 Episode 的记录,应该都是空数组了:
managedContext.delete(series)
CoreDataManager.shared.saveContext(managedContext)
let episodeRequest = NSFetchRequest<Episode>(entityName: "Episode")
let emptySeries = try managedContext.fetch(seriesRequest)
let emptyEpisode = try managedContext.fetch(episodeRequest)
assert(emptySeries.count == 0)
assert(emptyEpisode.count == 0)
观察表达记录的对象和表达关系的属性
观察了 cascade 类型的关系的删除效果之后,我们再来看看删除记录前后,表达这些记录的对象的值的变化。为此,我们把之前的代码改成这样:
func deleteSeries2() {
let seriesRequest = NSFetchRequest<Series>(entityName: "Series")
let episodeRequest = NSFetchRequest<Episode>(entityName: "Episode")
do {
let series = try managedContext
.fetch(seriesRequest)
.filter { $0.id == 1 }
.first!
let episodes = try managedContext
.fetch(episodeRequest)
print(series)
print(episodes)
managedContext.delete(series)
CoreDataManager.shared.saveContext(managedContext)
print(series)
print(episodes)
}
catch let error as NSError {
print(error.localizedDescription)
}
}
在删除 Series 记录前后,我们分别把之前读取的 series
和 episodes
打印在了控制台上。删除前,它们的值是这样的:

而在删除 series
记录后,它们的值则变成了这样:

可以看到,从 core data 删除了记录之后,这些表示记录的对象仍旧是存在的,但是它们的 data
部分都变成了 <fault>
。实际上,如果此时你尝试访问它们的属性,得到的都会是 nil
或者 0 这样无意义的值。总之,使用了 cascade 类型的删除规则之后,一旦删除了记录,就不要再使用与之有关的对象了。
Nullify
接下来,我们删除 History 中的记录,它和 Episode 之间有一个 Nullify 类型的关系。按照之前说过的,删除 History 记录,与之对应的 Episode 记录并不会被删除。我们用下面的代码验证下:
func deleteHistory() {
let historyRequest = NSFetchRequest<History>(entityName: "History")
let episodeRequest = NSFetchRequest<Episode>(entityName: "Episode")
do {
let history = try managedContext
.fetch(historyRequest)
.filter { $0.episodeId == 1 }.first!
var episode = try managedContext
.fetch(episodeRequest)
.filter { $0.id == 1 }.first!
managedContext.delete(history)
try managedContext.save()
print(history)
print(episode)
episode = try managedContext
.fetch(episodeRequest)
.filter { $0.id == 1 }.first!
print(episode)
}
catch let error as NSError {
print(error.localizedDescription)
}
}
首先,还是先从 core data 中读取了 id 为 1 的 Episode 记录,以及对应的 History 记录。要注意的是,不要对 fetch
方法返回的结果中记录的顺序做任何假设,数组中的第一个元素的 id 未必就是 1。为此,我们使用 filter
筛选了 fetch
的返回结果。实际上,core data 内置了功能非常强大的搜索和排序功能,不过为了不让问题分散,当前,就取出所有记录之后手工筛选就好了,毕竟当前的记录数很少。稍后等真正会用到查询功能时,我们再专门讨论。
其次,用类似的方法,我们从 core data 中删除了 episodeId 为 1 的 History 记录。这时,接下来的两个 print
语句打印的结果是类似这样的:

根据之前的经验,此时 history
对象应该已经“废”了,它已经不包含任何有意义的数据。但 episode
对象仍旧包含视频信息,只不过 episode.history
被设置成 nil
了。并且,当我们重新从 core data 中查询对应 Episode 记录的时候,会发现这条记录还在,只是对应的 history 字段,变成了 NULL:

这就是 Nullify 的作用。
Deny
这一节最后,我们来看 Deny。为了演示这个配置的效果,我们把 Episode 中的 history 关系的删除规则,设置成 Deny:

然后,重新执行下 deleteSeries1
函数。接下来:
- Series 中的 episodes 关系会导致对应的 Episode 记录一并被删除;
- 但 Episode 记录中有关联的 History 记录,因此会导致删除 Episode 记录失败;
由于我们在 deleteSeries1
方法中捕获了 core data 异常,这时在控制台,可以看到类似下面这样的消息:

其中,最后一行,是我们打印的 error.localizedDescription
,其余的部分,是 Xcode 打印的 core data 调试信息,对错误进行了详细描述。
What's next?
以上,就是各种 Delete Rule 在删除记录时发挥的不同作用。当然,我们没有演示 No Action 的情况,因为在真实的案例中,几乎不会用到这种配置。在删除记录时忽略数据之间的关联性有非常大的概率会给我们的代码带来一些潜在的、难以调试的问题。
至此,对于 core data 增删改查的基本操作,我们就说的差不多了。有了这些背景知识之后,下一节,我们来聊聊 core data 工作方式背后的细节。