肘子的 Swift 记事本

掌握 Core Data 中的关系:实战

发表于

为您每周带来有关 Swift 和 SwiftUI 的精选资讯!

在之前的文章 掌握 Core Data 中的关系:基础 中,我们已经对 Core Data 中的关系基本概念和使用原则进行了探讨。本文将在此基础上进一步,分享关于处理 Core Data 关系的一些实用经验和技巧。目的是为了帮助开发者更有效地利用 Core Data 框架的关系功能,提高开发的灵活性和效率。

本文旨在为已具备一定 Core Data 关系知识和实践经验的读者提供进阶理解和应用的视角,并非旨在提供一个全面的教程。

可选值

在 Xcode 的模型编辑器中定义实体属性时,开发者应区分编辑器中的 Optional 选项和 Swift 语言中的 Optional 类型,两者并非一回事。在 Core Data 中,Optional 选项意味着相应的 SQLite 字段可以接受 NULL 值。而 Swift 中的 Optional 类型则是一种语言层面的特性,表示变量可能为 nil。在 Core Data 模型中,这两种 Optional 的使用取决于具体场景和开发者的需求,并不需要严格对应。

在 Core Data 中,如果模型端的某个属性被标记为 Optional,则在相应的 Swift 代码中,这个属性可以被定义为 Non-Optional。这种做法提供了更多的灵活性,允许开发者根据实际应用场景来决定是否在代码中使用 Swift 的 Optional 类型。

有关 Core Data 可选值方面更详尽的内容,请阅读 Ask Apple 2022 中与 Core Data 有关的问答 (下)

举例来说,假设有 ItemTag 两个实体,它们之间是 One-to-One 关系。在使用 Core Data with CloudKit 时,这些关系在模型编辑器中必须标记为 Optional。但在实际应用中,如果这两个实体的实例之间总是彼此关联,即它们的关系总是有值的,那么在 Swift 代码中,可以将它们调整为非可选类型。这样做的好处是,可以在代码中更方便地访问这些属性,无需频繁解包。

Item_Model

Tag_Model

Core Data 默认生成的代码如下:

Swift
extension Item {
    @NSManaged public var timestamp: Date?
    @NSManaged public var tag: Tag?  // Optional
}

extension Tag {
    @NSManaged public var name: String?
    @NSManaged public var item: Item? // Optional
}

但你可以根据实际情况将它们调整为非可选类型:

Swift
extension Item {
    @NSManaged public var timestamp: Date
    @NSManaged public var tag: Tag  // Non-Optional
}

extension Tag {
    @NSManaged public var name: String // None-Optional 
    @NSManaged public var item: Item // Non-Optional
}

这样,就可以更加方便的在代码中获取数据了,这样调整的前提是开发者必须确保在读取属性前已经给属性进行了赋值

Swift
Text(item.tag.name)

Core Data 集合类型的 Swift 化

在处理 Core Data 的 to-Many 关系时,尤其是涉及有序关系时,调整其在 Swift 代码中的表示方式可带来显著的好处。

例如,考虑将 tag 改成有序的对多关系 tags

Tags_Model

Core Data 自动生成的代码如下:

Swift
extension Item {
    @NSManaged public var timestamp: Date?
    @NSManaged public var tags: NSOrderedSet?
}

为了提高代码的可读性和易用性,我们可以考虑将 NSOrderedSet? 类型转换为 Array<Tag>。这样的调整不仅减少了解包处理的需要,而且使得 tags 属性更加符合 Swift 语言的习惯用法,如使用下标和迭代器进行访问。

Swift
extension Item {
    @NSManaged public var timestamp: Date?
    @NSManaged public var tags: Array<Tag>
}

调整后,我们可以更方便地在 Swift 中操作这些数据,例如( Array 符合 RandomAccessCollection 协议 ):

Swift
ForEach(item.tags){ tag in
    Text(tag.name ?? "")
}

值得注意的是,在处理非有序的 to-Many 关系时,将其转换为 Array 类型可能并非最佳选择。这主要是因为非有序集合的本质特性以及它们在 Core Data 中的管理方式。在 Core Data 中,非有序关系通常以 NSSet 表示,这直观反应了集合的无序性和元素的唯一性。如果转换为 Array 类型,这些关键特性可能会在字面上丢失。因此,对于非有序关系,使用 Swift 中的 Set 类型通常是更合适的选择。

例如,对于 Item 实体的 tags 属性,如果它是非有序的可选的 to-Many 关系,可以在 Swift 中这样表示:

Swift
extension Item {
    @NSManaged public var timestamp: Date?
    @NSManaged public var tags: Set<Tag>
}

这种方式既保持了集合的无序性和唯一性,又使得代码更加符合 Swift 的使用习惯,提高了代码的可读性。

Count

在处理 to-Many 关系时,经常需要获取关联对象的数量。虽然直接使用 .count 属性是一种常见方法,但开发者还可以考虑使用派生属性(Derived Attribute)来更高效地获取这一计数。

例如,在下图所示的情况中,我们为 TodoList 实体创建了一个名为 count 的派生属性。这样,开发者可以通过简单地访问 todolist.count 来直接获取与 TodoList 相关联的 items 对象的数量。这种方法使得获取关联对象数量变得直观且高效。

Derived

相较于直接调用关系的 .count 属性,使用派生属性来计算数量通常更加高效。这是因为派生属性采用了不同的统计机制——它们在写入数据时计算并保存计数值,而在读取数据时则直接使用这个预先计算好的值。这种机制特别适用于那些读操作远多于写操作的场景。

然而,派生属性的一个重要限制在于,它们仅能统计已经持久化的数据量。这意味着如果有数据尚未被保存到持久化存储中,即处于暂存状态,这些数据将不会被派生属性的计数所考虑。因此,在使用派生属性时,开发者需要留意这一点,确保他们的数据处理逻辑考虑到了这种统计方式的这一特性。

想要更深入了解派生属性的使用方法,建议阅读《如何在 Core Data 中使用 Derived 和 Transient 属性》,这篇文章详细介绍了派生属性的应用技巧。

管理非有序的 to-Many 数据

在许多实际应用场景中,to-Many 关系往往是无序的,这一点在使用 Core Data with CloudKit 时尤为明显,因为它不支持有序关系。

当直接通过关系属性获取数据(如下面的示例代码所示),Core Data 并不能保证返回数据的顺序:

Swift
let tags = Array(items.tags)

Core Data 在底层绝大多数情况下都会使用 SQLite 数据库来存储数据。在数据库中,除非明确指定排序顺序,否则记录的检索顺序是不确定的。

因此,为了保证获取非有序的 to-Many 数据时结果的一致性,建议不要直接使用关系属性。相反,应当创建一个包含谓词和排序条件的 NSFetchRequest 来执行查询,如下所示:

Swift
func fetchTagsBy(item:Item) -> [Tag] {
    let request = NSFetchRequest<Tag>(entityName: "Tag")
    request.predicate = NSPredicate(format: "item = %@", item)
    request.sortDescriptors = [NSSortDescriptor(keyPath: \Tag.name, ascending: true)]
    return (try? viewContext.fetch(request)) ?? []
}

在 SwiftUI 开发中,推荐将展示 to-Many 数据的界面单独封装成独立的视图,并通过 @FetchRequest 来获取数据。这种做法不仅确保了数据获取顺序的稳定性,还能及时响应数据变化,并使视图更新更加高效:

Swift
struct TagsList: View {
    @FetchRequest var tags: FetchedResults<Tag>
    init(item: Item) {
        let request = NSFetchRequest<Tag>(entityName: "Tag")
        request.predicate = NSPredicate(format: "item = %@", item.objectID) // 使用 NSManagedObject 和 NSManagedObjectID ,生成的 SQL 指令都一样
        request.sortDescriptors = [NSSortDescriptor(keyPath: \Tag.name, ascending: true)]
        _tags = FetchRequest(fetchRequest: request)
    }

    var body: some View {
        List(tags) { tag in
            TagDetail(tag: tag)
        }
    }
}

struct TagDetail: View {
    @ObservedObject var tag: Tag
    var body: some View {
        Text(tag.name)
    }
}

对多关系与子查询

在上一篇文章中,我们提到关系可以在某些场景下提高查询效率,并丰富查询手段。Core Data 中的子查询(SubQuery)功能正是这一场景的典型示例。

子查询是 Core Data 框架中一种高效的查询技术,它允许开发者在一个已有的查询结果集上执行更为复杂的查询。这在处理复杂的数据模型时尤其有用,特别是当需要根据关联对象的属性来进行过滤时。

子查询的基本格式如下所示:

Swift
SUBQUERY(collection, $x, condition)
  • collection 是要查询的集合,通常是一个对多关系属性。
  • $x 是一个变量代表集合中的每个元素( 可以任意设定名称 )。
  • condition 是应用于集合中每个元素的条件。

举个例子,假设我们想要获取所有至少有一个 Tag 名称以 “A” 开头的所有的 Item。可以使用以下的 NSPredicate 表达式:

Swift
NSPredicate(format: "SUBQUERY(tags, $tag, $tag.name BEGINSWITH 'A').@count > 0")

使用 Swift 高阶函数在内存中进行同样的操作对应如下的代码:

Swift
let result = items.filter { item in
    item.tags.contains { tag in
        tag.name.hasPrefix("A")
    }
}

子查询是直接在 SQLite 端执行的,这意味着无论是在性能上还是在内存占用上,都比在内存中进行筛选要高效得多。此外,所有的筛选和排序操作也建议在 SQLite 端进行,即通过使用精心设计的谓词和排序条件来实现。这样的做法不仅提高了数据处理的效率,还有助于减轻应用程序内存的负担,特别是在处理大型数据集时。

接下来

在本文中,我们探讨了在实际开发场景中应用 Core Data 关系的一系列技巧。实际上,一旦开发者掌握了关系的基础理论和内部机制,他们就能不断地在实践中总结和发掘出更适合自己项目的方法和经验。

下一篇文章将专注于 Core Data 的后继框架 — SwiftData。我们不仅会探究 SwiftData 在处理数据关系方面的变化,还会审视这些变化的适用性,特别是在其首个版本中如何有效避免关系操作带来的潜在性能问题。

我非常期待听到您的想法! 请在下方留下您的评论 , 分享您的观点和见解。或者加入我们的 Discord 讨论群 ,与更多的朋友一起交流。

Fatbobman(东坡肘子)

热爱生活,乐于分享。专注 Swift、SwiftUI、Core Data 及 Swift Data 技术分享。欢迎关注我的社交媒体,获取最新动态。

你可以通过以下方式支持我