掌握 Core Data 中的关系:基础

发表于

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

在众多关于 Core Data 的讨论中,“对象图管理”无疑是一个频繁出现的核心概念。作为一个颇具盛名的对象图管理框架,Core Data 如何精确描述并有效管理不同数据实例之间的复杂关系,成为了它的关键任务。事实上,管理关系的能力不仅构成了 Core Data 的核心特征,也是其相较于其他数据持久化框架的一大显著优势。在本文中,我们将深入探讨 Core Data 中关系的基本概念,同时提供关于实现这些关系的重要指导和建议。

在本文中,我们将深入探讨与关系相关的基础知识。这些概念不仅在 Core Data 中至关重要,同样也适用于其后继框架——SwiftData。

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

关系的定义

在 Core Data 的世界里,关系是构筑实体(Entity)间相互联系的桥梁,它决定了一个实体如何对另一个实体产生影响。在大多数情况下(抽象实体除外),Core Data 中的每一个实体定义都对应着 SQLite 数据库中的一个表。因此,从底层实现的角度来看,Core Data 中的关系可以被看作是一种在不同表间建立联系和进行操作的机制。

关系的类型

在 Core Data 的架构中,实体间关系的描述方式多样。从实体间的引用角度出发,这些关系大致分为单向关系和双向关系。

单向关系存在于当一个实体(A)引用另一个实体(B),但 B 不反向引用 A 的情形。尽管在特定场景下,如 A 需要了解 B 的信息而 B 不需要知道 A 的详情时,单向关系可以满足要求,但考虑到数据的完整性和对象图的维护,双向关系往往是更优选择。

双向关系则是指当一个实体(A)引用另一个实体(B),同时 B 也反向引用 A 的情况。这种关系使得 Core Data 能够更有效地管理对象间的联结,并为开发者提供了从多个实体角度调用其他关联实体的更大灵活性。

在这种双向关系框架下,我们进一步可以将关系划分为三种主要类型:

  1. 一对一(One-to-One)关系:
    • 定义: 一个实体(A)中的单个实例与另一个实体(B)中的单个实例相关联。
    • 用途: 适用于两个实体间存在独特且直接的联系时。
    • 示例: 个人(Person)与其护照(Passport)的关系。
  2. 一对多(One-to-Many)关系:
    • 定义: 一个实体(A)中的单个实例与另一个实体(B)中的多个实例相关联。
    • 用途: 当一个实体能与另一个实体的多个实例建立联系时。
    • 示例: 用户(User)与其发布的多条帖子(Posts)。
  3. 多对多(Many-to-Many)关系:
    • 定义: 一个实体(A)中的多个实例与另一个实体(B)中的多个实例相互关联。
    • 用途: 适用于两个实体间的实例可以自由组合关联的场景。
    • 示例: 文章(Article)与标签(Tag)之间的关系,其中一篇文章可以拥有多个标签,同时不同的文章也可以使用相同的标签进行标记。

在 Core Data 中,构建的关系越多,对象图越丰富,也就越复杂,这对 Core Data 的对象图管理能力构成了挑战。

逆向关系

在 Core Data 中,当我们建立双向关系后,框架会要求我们为这些关系指定逆向关系(Inverse Relationship)。虽然苹果的官方文档明确要求必须设定逆向关系,但由于在许多情况下,即使未设置逆向关系,应用程序也能正常运行,因此一些开发者对是否真的需要设置逆向关系产生了疑问。

为了更好地理解在 Core Data 中使用逆向关系的必要性,我们需要深入了解逆关系的概念及其作用。尽管我们可能已经在两个实体间使用了双向关系,但如果不设置逆关系,对其中一侧实体的修改可能不会自动反映到另一侧。逆关系是一种数据模型设计概念,它在两个实体间的关系管理中起着关键作用。

逆关系的设置带来以下好处:

  • 数据完整性

    这是一个常见的疑问点。虽然开发者在未设置逆关系的情况下可能从未遇到数据不一致的问题,但在某些特定情况下,不设置逆关系可能导致预期之外的结果。

    考虑这样一个例子:假设 A 和 B 之间存在着一个 One-to-One、非可选的关系,且其删除规则设为 Cascade。同时,B 和 A 之间也是 One-to-One、非可选关系,但删除规则为 Nullify。在未设置逆关系的情况下,直接删除 B 的实例的操作是被允许的,这与预期相悖,因为根据上述设置,删除操作理应只能从 A 端发起。如果设置了逆关系,Core Data 就会从 A 的角度检查是否允许执行删除操作。由于发现 A 与 B 之间是非可选的关联,Core Data 会阻止删除操作,从而确保数据的完整性和一致性得到维护。

    逆关系在维护数据完整性方面发挥着关键作用。当你在一个实体中更新关系时,其逆关系会自动进行更新,确保不同实体间的数据保持同步和一致。

  • 查询优化

    Core Data 根据其所掌握的信息生成 SQL 语句。逆关系的存在可以丰富这些信息,从而优化查询条件和提升查询效率。

  • 高效的内存管理

    尽管官方文档中没有直接提到逆关系对内存管理的作用,但通过理解 Core Data 中的对象和引用管理原则,我们可以推断出逆关系对内存管理的潜在益处。逆关系简化了引用管理,确保对象根据对象图的当前状态得到适当的保留或释放。

因此,尽管某些开发者可能认为设定逆关系对他们本身看似无关紧要,它实际上为 Core Data 提供了重要的信息,帮助框架更有效地进行对象图管理。

在 SwiftData 中,对于某些关系类型,即使开发者没有显式设置逆关系,SwiftData 也会自动在数据模型中补充逆关系信息。

删除规则

在 Core Data 中,删除规则(Delete Rules)起着关键作用,它们定义了在删除一个实体对象时如何处理与该对象关联的其他实体对象。选择合适的删除规则对于维护数据完整性和避免数据库中的悬空引用极为重要。Core Data 提供了四种基本的删除规则:

  1. Nullify(置空):
    • 应用此规则时,删除对象会导致所有相关联对象的对应关系属性被置为空。
    • 这种方式相当于“解除关系”,而不是删除关联对象,仅移除双方的连接。
    • 通常适用于关系中的弱侧。例如,在一个主题(Topic)和多个附件(Attachment)的关系中,删除一个附件不会导致主题被删除。
  2. Cascade(级联):
    • 在删除对象时,所有与之关联的对象也会被删除。
    • 适用于强依赖关系,即关联对象在主对象不存在时没有存在意义。
    • 例如,如前文逆关系部分所述,A 作为关系中强侧的一方,采用了 Cascade 规则,这意味着删除 A 也会导致所有与 A 直接相关的 B 实体被删除。
  3. Deny(拒绝):
    • 若关联对象仍存在,则阻止删除操作。
    • 此规则用于确保不会因删除一个对象而产生悬空引用。
    • 例如,家庭成员对象不能独立于家庭存在,那么在家庭中至少存在一个成员的情况下,不允许删除家庭对象。
  4. No Action(无操作):
    • 删除对象时,不对关联对象做任何处理。
    • 可能会造成悬空引用,因此使用时需特别小心。
    • 实质上,这意味着开发者需自行处理相关的删除逻辑,而非由 Core Data 自动管理。

在设计数据模型时,选择合适的删除规则至关重要,因为它直接影响数据的完整性和应用的逻辑稳定性。通常情况下,NullifyCascade 因为能够有效管理关系的生命周期而被广泛使用,而 DenyNo Action 则需要在特定场景下谨慎使用。

有序关系

在 Core Data 中,to-Many 关系分为无序和有序两种形式。默认情况下,“对多”关系是无序的(对应 NSSet 类型,保证对象的唯一性,但不保证顺序)。开发者可以通过在数据模型中勾选 ordered 选项来将此关系设置为有序(对应 NSOrderedSet 类型)。

虽然官方文档和大多数第三方文章通常不会详细解释有序关系的底层实现,但通过分析其在 SQLite 中的数据结构,我们可以对其基本逻辑有所了解。

简而言之,对于有序关系的一方,Core Data 会创建一个类似索引的内部属性(字段)。例如,在 Item 和 Tag 之间存在一对多的有序关系时,Core Data 会在 Tag 对应的表中添加一个 Z_FOK_ITEM 字段,并按顺序给它填充数字。为了避免因位置调整而频繁地更新所有索引,Core Data 在相邻两个位置之间预留了一定的数字空间。

image-20240102100725036

有序关系的主要优势在于它提供了一系列方便的预置方法,如 insertIntoTags(_:at:)replaceTags(at:with:) 等。这些方法只适用于操作隐藏的索引属性。然而,在更多的情况下,开发者通常会根据自己的需求创建专门用于排序的属性和相应的顺序处理机制。

关系与批量操作

在 Core Data 中,关系的验证和操作通常是在托管对象上下文(NSManagedObjectContext)中执行的。由于批量操作绕过了上下文处理,因此这些操作往往会忽略数据模型中设置的大多数关系规则(如验证、删除规则等)。然而,在进行批量删除操作时,Core Data 仍会自动处理以下两种情况:

  • 采用 Cascade 删除规则的关系:

    例如,假设 Item 实体有一个名为 attachment 的关系(无论是一对一还是一对多),且该关系在 Item 端的删除规则设置为 Cascade。当对 Item 实体进行批量删除操作时,Core Data 会自动删除所有与之关联的 Attachment 实体。

  • 删除规则为 Nullify 且关系为可选:

    假如 Item 实体有一个名为 attachment 的关系(无论是一对一还是一对多),且该关系在 Item 端的删除规则设置为 Nullify,并且关系被标记为可选(Optional)。在这种情况下,当对 Item 实体进行批量删除操作时,Core Data 会将所有相关 Attachment 实体中的关系 ID(指向 Item)设置为 NULL,但并不会删除这些 Attachment 实体。

想要深入了解 Core Data 中的批量操作,建议阅读 “如何在 Core Data 中进行批量操作” 这篇文章。

关系的懒加载特性及其应用

在 Core Data 中,托管对象的按需数据填充是一个关键功能。默认情况下,从持久化存储库中检索的托管对象初始状态为“惰性”(Fault),这意味着对象数据不会立即完全加载。只有当真正需要访问该对象的特定属性时,Core Data 才会从行缓冲区或持久化存储中加载完整数据(即转为“实现”状态,Fulfilled)。为了优化性能,开发者可以通过设置 NSFetchRequestreturnsObjectsAsFaults 属性为 false 来在初次获取数据时即刻完成数据的加载,从而避免后续的二次 IO 操作。

这种按需加载的机制使得 Core Data 在性能效率和内存占用之间达到了良好的平衡。

然而,即使 returnsObjectsAsFaults 被设置为 false,Core Data 也不会自动加载与当前实例相关的其他关系数据。如果开发者希望在获取特定实体 A 的同时预先加载其相关联的实体 B,可以在 NSFetchRequestrelationshipKeyPathsForPrefetching 属性中指定相关关系。例如:

Swift
let request = NSFetchRequest<Item>(entityName: "Item")
request.relationshipKeyPathsForPrefetching = ["Tag"]

正是由于这种懒加载特性,开发者在设计数据模型时会采取一些策略来优化性能和内存使用。例如,对于一个包含图片的 Image 实体,如果通常只需要显示图片的缩略图,可以将完整图片数据放在一个单独的 ImageData 实体中,并通过关系连接。这样,可以充分利用懒加载特性,在需要时才加载完整的图片数据,从而减少内存占用并提高应用的响应速度。

Swift
class Image: NSManagedObject {
  @NSManaged var thumbnail: Data
  @NSManaged var Post: Post?
  @NSManaged var data: ImageData?
}

class ImageData: NSMangedObject {
  @NSManged var data: Data
}

关系是如何在 SQLite 中标识的

Core Data 利用了在同一个数据库中仅需依靠 Z_ENT + Z_PK 即可定位记录的特性来实现了在不同的实体之间标注关系的工作。为了节省空间,Core Data 仅保存了每个关系记录的 Z_PK 数据,Z_ENT 则直接由数据模型从 Z_PRIMARYKEY 表中获取。

在数据库中,创建关系的规则如下:

  • 一对多关系:

    在“一”的一方不会创建新字段,而在“多”的一方会为关系创建新字段,该字段保存“一”方的 Z_PK 值。字段的名称通常是“Z”加上关系名称(大写形式)。

  • 一对一关系:

    关系的两端都会添加新字段,各自保存对方数据的 Z_PK 值。

  • 多对多关系:

    在关系的两端都不添加新字段,而是创建一个新的关联表来表示这种多对多关系。该表中逐行添加关系两端数据的 Z_PK 值。

    例如,在下图所示的情况中,Item 和 Tag 之间存在多对多关系,Core Data 为此创建了 Z_2TAGS 表来管理这两个实体之间的关系数据。

relationship

想要更深入了解 Core Data 如何在 SQLite 中存储数据,请参阅文章 “Core Data 是如何在 SQLite 中保存数据的” 以获取更多底层存储实现的细节。

在 Core Data with CloudKit 中的关系设置要求

随着 Core Data with CloudKit 的日益普及,越来越多的开发者开始在他们的应用中集成这项技术,以便为用户提供云存储和跨设备数据同步功能。在启用 Core Data with CloudKit 功能时,需要遵循一些特定的关系设置规则:

  • 关系必须设为可选(Optional
  • 必须定义逆向关系
  • 不支持 Deny 的删除规则
  • 不支持有序关系

在设计支持 CloudKit 的 Core Data 数据模型时,遵循这些规则非常重要,因为它们直接影响到云数据的同步机制和整个应用的数据完整性。

欲了解更多关于 Core Data with CloudKit 的细节,请参阅 “Core Data with CloudKit 系列文章”。

持久化存储内部的关系限制

在 Core Data 的应用中,虽然我们可以通过在模型编辑器中设置多个配置(Configurations),对应于不同的持久化存储描述(NSPersistentStoreDescription),来在不同的持久化存储中保存多种实体配置,但存在一定的限制。

具体而言,关系的创建只能局限于同一个持久化存储或配置内的实体之间。这意味着,关系只能在同一个 SQLite 数据库文件中的不同表间建立。这是由于 Core Data 的底层存储逻辑决定的,它依赖于相同数据库文件内的表之间的关联,以维护数据的完整性和一致性。

因此,当设计涉及多个持久化存储的 Core Data 应用时,需要特别注意这一点,确保所有需要相互关联的实体都位于同一个配置内。

在什么情况下考虑使用关系

在 Core Data 模型中合理地使用关系,对于表示实体间的联系、提高数据操作效率和保证数据完整性都至关重要。以下是在什么情况下考虑使用关系的一些建议:

  1. 多实体引用同一类型数据:当某种类型的数据可能被多个实体引用时,应该将其定义为一个单独的实体,并通过关系进行关联。这避免了在多个实体中重复定义相同的属性。
  2. 实体间的业务逻辑关联:当两个或多个实体在业务逻辑上存在关联时,应通过关系来反映这种关联,例如作者与其书籍的关系。
  3. 一对多或多对多复杂关系:在需要表达一对多或多对多的复杂关系时,关系成为模型的一个关键元素。
  4. 数据一致性:通过设置关系的删除规则,可以在删除一个实体时自动处理相关联的数据,保持数据的一致性。
  5. 优化查询效率:建立关系可以提高从一个实体获取关联实体数据的查询效率,并丰富查询手段。
  6. 模型的清晰度和可维护性:关系可以明确地表示模型中的关联,提高整个数据模型的清晰度和可维护性。
  7. 优化内存占用:对于不常用但占用资源较多的数据,可以通过关系的方式进行关联,优化内存使用。

总之,在数据模型中存在业务关联、需要表示复杂关系或者要求数据完整性的情况下,应该充分利用 Core Data 的关系特性,使模型更加清晰、高效。这不仅有助于提升应用的性能和健壮性,还能简化开发和维护过程。

接下来

在这篇文章中,我们对 Core Data 中的关系概念进行了基础性的介绍和整理。在下一篇文章中,我们将通过实际的代码示例来讨论在开发过程中运用 Core Data 关系的一些技巧和经验。例如,如何在类型声明中修改关系类型,如何有效地设置和获取关系数据,以及如何在构建查询的谓词时更加巧妙地利用这些关系。这些内容旨在提供更实践的指导,帮助开发者在具体开发实践中更好地应用 Core Data 的关系管理特性。

与全球开发者一同,每周探索 Swift 世界的精彩内容

如果文章对你有所帮助,可以请我喝杯