NSManagedObjectID 与 PersistentIdentifier:掌握 Core Data 与 SwiftData 中的数据标识符

发表于

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

Core Data 和 SwiftData 是苹果为开发者设计的强大数据管理框架,能够高效处理复杂的对象关系,因而被称为对象图管理框架。在这两个框架中,NSManagedObjectIDPersistentIdentifier 功能相似,且都极为重要。本文将深入探讨它们的功能、使用方法及注意事项。

什么是 NSManagedObjectID 和 PersistentIdentifier?

在 Core Data 和 SwiftData 中,NSManagedObjectIDPersistentIdentifier 分别作为数据对象的“身份证”,用于让系统能够在持久化存储中准确找到相应的记录。它们的主要功能是帮助应用程序在不同的上下文和生命周期内,正确识别和管理数据对象。

在 Core Data 中,托管对象的 objectID 属性可以获取对应的 NSManagedObjectID

Swift
let item = Item(context: viewContext)
item.timestamp = Date.now
try? context.save()
let id = item.objectID // NSManagedObjectID

而在 SwiftData 中,可以通过数据对象的 idpersistentModelID 属性获取相应的 PersistentIdentifier

Swift
let item = Item(timestamp: Date.now)
modelContext.insert(item)
try? modelContext.save()
let id = item.persistentModelID // PersistentIdentifier

值得注意的是,Core Data 中 NSManagedObject 的默认 id 属性实际上是 ObjectIdentifier,而非 NSManagedObjectID。如果需要,可以通过扩展重新声明该属性,将其修改为 NSManagedObjectID

Swift
public extension NSManagedObject {
  var id: NSManagedObjectID { objectID }
}

为了简化后文,除非特别需要区分,我将使用“标识符”作为 NSManagedObjectIDPersistentIdentifier 的统称。

临时 ID 与永久 ID

在 Core Data 和 SwiftData 中,当一个数据对象刚创建且尚未持久化时,其标识符为临时状态。临时标识符无法在跨上下文中使用,即无法在另一个上下文中获取对应的数据。

在 Core Data 中,NSManagedObjectID 提供了 isTemporaryID 属性,用来判断标识符是否为临时状态:

Swift
let item = Item(context: viewContext)
item.timestamp = Date.now
// 数据未保存
print(item.objectID.isTemporaryID) // true

然而,在 SwiftData 中,当前并没有类似的属性或方法可以直接判断 PersistentIdentifier 的状态。由于 SwiftData 的 mainContext 默认启用了 autoSave 功能(开发者无需显式保存数据),因此在创建数据对象后,标识符可能暂时无法在其他上下文中使用。如果遇到这种情况,可以通过手动显式保存来避免该问题。

标识符与持久化数据的对应关系

永久 ID(即持久化后的标识符)中包含了足够的信息,框架可以依赖这些信息在数据库中定位到相应的数据。当我们打印一个永久 ID 时,可以看到其详细内容:

Swift
print(item.objectID)
// 0xa264a2b105e2aeb2 <x-coredata://92940A15-4E32-4F7A-9DC7-E5A5AB22D81E/Item/p28>
  • x-coredata:Core Data 使用的自定义 URI 协议,表示这是一个 Core Data 的 URI。
  • 92940A15-4E32-4F7A-9DC7-E5A5AB22D81E:持久化存储的唯一标识符,通常是一个 UUID,用于标识存储文件的位置(例如,对应的数据库文件)。这个标识符是在数据库文件创建时生成的,通常不会改变,除非手动修改存储的元数据(尽管这种操作极为罕见,也不建议在实际项目中轻易修改)。
  • Item:数据对象对应的实体名称,对应 SQLite 中存储该实体数据的表。
  • p28:表明数据在该实体表中的具体位置。对象保存后,Core Data 会为其生成唯一的标识号。

了解更多数据保存机制,请参阅 Core Data 是如何在 SQLite 中保存数据的

对于临时 ID,未保存的对象会缺少表中的标识号,如下所示:

Swift
item.objectID.isTemporaryID // true, 临时 ID
print(item.objectID)
// x-coredata:///Item/t6E5D1507-3E60-41F0-A5F7-C1F28DC63F402

SwiftData 的默认实现仍基于 Core Data,因此 PersistentIdentifier 的格式与 NSManagedObjectID 十分相似:

Swift
print(item.persistentModelID)
// SwiftData.PersistentIdentifier(id: SwiftData.PersistentIdentifier.ID(url: x-coredata://A07B3AB6-F28D-4F15-9B5D-9B12EB052BC6/Item/p1), implementation: SwiftData.PersistentIdentifierImplementation)

PersistentIdentifier 处于临时状态时,它同样缺少表中的标识号:

Swift
// PersistentIdentifier(id: SwiftData.PersistentIdentifier.ID(url: x-swiftdata://Item/3BD56EA6-831B-4B24-9B2E-B201B922C91D), implementation: SwiftData.PersistentIdentifierImplementation)

通过 持久化存储 ID + 表名 + 标识号,可以唯一定位到某个永久 ID 对应的数据。任何一个部分的变化都会指向不同的数据。因此,无论是 NSManagedObjectID 还是 PersistentIdentifier,它们都只能在同一设备上使用,无法跨设备识别数据。

标识符是 Sendable 的

无论是 Core Data 还是 SwiftData,数据对象只能在特定的上下文(线程)中使用,否则很容易出现并发问题,甚至导致应用崩溃。因此,在不同上下文之间传递数据时,只能使用它们的标识符。

PersistentIdentifier 是一个结构体,天生是线程安全的,并且被标注为 Sendable

Swift
public struct PersistentIdentifier : Hashable, Identifiable, Equatable, Comparable, Codable, Sendable

NSManagedObjectID 作为 NSObject 的子类,早期并没有明确的线程安全标注。

Ask Apple 2022 中,苹果工程师确认它是线程安全的,开发者可以使用 @unchecked Sendable 进行标注。从 Xcode 16 开始,Core Data 框架已经对此进行了官方标注,开发者无需再手动标注。

Swift
open class NSManagedObjectID : NSObject, NSCopying, @unchecked Sendable

因此,标识符在 Core Data 和 SwiftData 中是确保安全并发操作的关键。

请阅读 SwiftData 中的并发编程Core Data 并发编程提示 了解更多有关并发操作的内容。

如何通过标识符获取对应的数据

获取数据的方法主要可以分为两类:基于谓词的查询和使用上下文或 ModelActor 提供的直接方法。

基于谓词

在 Core Data 中,可以通过构建谓词,根据 NSManagedObjectID 来检索数据:

Swift
// 根据单个 ID 获取
let id = item.objectID
let predicate = NSPredicate(format: "SELF == %@", id)

// 批量获取
let ids = [item1.objectID, item2.objectID, item3.objectID]
let predicate = NSPredicate(format: "SELF IN %@", ids)

在 SwiftData 中,同样可以通过类似的方式构建谓词:

Swift
// 根据单个 ID 获取
let id = item.persistentModelID
let predicate = #Predicate<Item> {
  $0.persistentModelID == id
}

// 批量获取
let ids = [item1.persistentModelID, item2.persistentModelID]
let predicate = #Predicate<Item> { item in
    ids.contains(item.persistentModelID)
}

需要特别注意的是,在 SwiftData 中,虽然 PersistentModelid 属性也是 PersistentIdentifier,但在谓词中只能使用 persistentModelID 进行检索。

使用上下文获取单个数据

Core Data 的 NSManagedObjectContext 提供了三种不同的方式来根据 NSManagedObjectID 获取数据,区别如下:

  • existingObject(with:)

    如果上下文中已存在指定对象,该方法会返回该对象;否则,它会从持久化存储中获取并返回完整实例化的对象。与 object(with:) 不同,它不会返回一个未初始化的对象。如果对象既不在上下文也不在存储中,会抛出错误。换句话说,只要数据存在,该方法必定返回完整对象。

    Swift
    func getItem(id: NSManagedObjectID) -> Item? {
      guard let item = try? viewContext.existingObject(with: id) as? Item else { return nil }
      return item
    }
  • registeredModel(for:)

    此方法仅返回当前上下文中已注册的对象(标识符相同)。如果找不到该对象,会返回 nil,但这并不意味着数据不存在于存储中,只是未在当前上下文中注册。

  • object(with:)

    即使对象未注册,object(with:) 仍会返回一个占位对象。访问该占位对象时,上下文会尝试从存储中加载数据。如果数据不存在,可能会导致崩溃。

在 SwiftData 中,也有类似的方法,其中 model(for:) 对应 object(with:)registeredModel(for:) 功能相同,而 existingObject(with:) 则通过 @ModelActor 宏构建的 actor 实例的下标方法实现:

Swift
@ModelActor
actor DataHandler {
  func getItem(id: PersistentIdentifier) -> Item? {
    return self[id, as: Item.self]
  }
}

更多关于如何在 Core Data 中实现 @ModelActor 和下标方法的内容,请参考 Core Data 改革:实现 SwiftData 般的优雅并发操作 一文。

NSManagedObjectID 实例仅在同一个协调器中有效

虽然 NSManagedObjectID 实例包含了足够的信息来指示持久化后的数据,但当你尝试在另一个 NSPersistentStoreCoordinator 实例中使用它时,即使使用相同的数据库文件,也无法获取到相应的数据。换句话说,NSManagedObjectID 实例不能跨协调器使用。

这是因为 NSManagedObjectID 实例中还包含了对应的 NSPersistentStore 实例的私有属性,NSPersistentStoreCoordinator 可能依赖这些私有属性来检索数据,而不仅仅是通过持久化存储的标识符来定位。

如何持久化标识符

为了安全地跨协调器使用标识符并实现其持久化,可以通过 NSManagedObjectIDuriRepresentation 方法生成只包含“持久化存储 ID + 表名 + 标识号”的 URI。持久化后的 URL 不仅可以跨协调器使用,即使在应用冷启动后,也能通过该 URL 恢复正确的数据。

Swift
let url = item.objectID.uriRepresentation()

对于 PersistentIdentifier,由于其遵循了 Codable 协议,最便捷的持久化方式是将其编码并保存:

Swift
let id = item.persistentModelID
let data = try! JSONEncoder().encode(id)

标识符的持久化在多个场景下都很有用。例如,当用户选择某个数据时,持久化该数据的标识符可以在应用冷启动后恢复到退出时的状态。或者在使用 Core Spotlight 框架时,可以将标识符添加到 CSSearchableItemAttributeSet 中,用户通过 Spotlight 搜索到数据后,可以直接通过标识符跳转到对应的数据视图。

更多信息请参阅 在 Spotlight 中展示应用中的 Core Data 数据

如何创建标识符

在 Core Data 中,虽然 NSManagedObjectID 没有公开的构造方法,但我们可以通过有效的 URL 来生成相应的 NSManagedObjectID 实例:

Swift
let container = persistenceController.container
if let objectID = container.persistentStoreCoordinator.managedObjectID(forURIRepresentation: uri) {
  let item = getItem(id: objectID)
}

在 iOS 18 中,Core Data 引入了一个直接通过字符串构建标识符的新方法:

Swift
let id = coordinator.managedObjectID(for: "x-coredata://92940A15-4E32-4F7A-9DC7-E5A5AB22D81E/Item/p29")!
let item = try! viewContext.existingObject(with: id) as! Item

在 SwiftData 中,可以利用 Codable 协议提供的功能,通过编码数据来创建 PersistentIdentifier

Swift
let id = item.persistentModelID
let data = try! JSONEncoder().encode(id) // 持久化 ID

// 通过编码数据构建 PersistentIdentifier
func getItem(_ data: Data) -> Item? {
  let id = try! JSONDecoder().decode(PersistentIdentifier.self, from: data)
  return self[id, as: Item.self]
}

iOS 18 中,SwiftData 还引入了另一种构建 PersistentIdentifier 的方法,展示了标识符的组成要素:

Swift
let id = try! PersistentIdentifier.identifier(for: "A07B3AB6-F28D-4F15-9B5D-9B12EB052BC6", entityName: "Item", primaryKey: "p1")
print(id)

// PersistentIdentifier(id: SwiftData.PersistentIdentifier.ID(url: x-developer-provided://A07B3AB6-F28D-4F15-9B5D-9B12EB052BC6/Item/p1), implementation: SwiftData.GenericPersistentIdentifierImplementation<Swift.String>)

不过,这种方法无法生成适用于默认存储(Core Data)的标识符,主要用于自定义存储实现。但我们可以利用此思路创建一个支持默认存储的标识符构造方法:

Swift
struct PersistentIdentifierJSON: Codable {
  struct Implementation: Codable {
    var primaryKey: String
    var uriRepresentation: URL
    var isTemporary: Bool
    var storeIdentifier: String
    var entityName: String
  }

  var implementation: Implementation
}

extension PersistentIdentifier {
  public static func customIdentifier(for storeIdentifier: String, entityName: String, primaryKey: String) throws
    -> PersistentIdentifier
  {
    let uriRepresentation = URL(string: "x-coredata://\(storeIdentifier)/\(entityName)/\(primaryKey)")!
    let json = PersistentIdentifierJSON(
      implementation: .init(
        primaryKey: primaryKey,
        uriRepresentation: uriRepresentation,
        isTemporary: false,
        storeIdentifier: storeIdentifier,
        entityName: entityName)
    )
    let encoder = JSONEncoder()
    let data = try encoder.encode(json)
    let decoder = JSONDecoder()
    return try decoder.decode(PersistentIdentifier.self, from: data)
  }
}

let id = try! PersistentIdentifier.customIdentifier(for: "A07B3AB6-F28D-4F15-9B5D-9B12EB052BC6", entityName: "Item", primaryKey: "p1")
print(id)

// PersistentIdentifier(id: SwiftData.PersistentIdentifier.ID(url: x-coredata://A07B3AB6-F28D-4F15-9B5D-9B12EB052BC6/Item/p1), implementation: SwiftData.PersistentIdentifierImplementation)

通过这种方式,可以轻松地根据 NSManagedObjectID 提供的 URL 中包含的信息构建适用于 SwiftData 的标识符,同时减少 PersistentIdentifier 持久化所占的空间。

为什么持久化标识符会失效

持久化标识符的构成主要包括:持久化存储 ID + 表名 + 标识号。以下几种情况最容易导致标识符失效:

  • 数据被删除
  • 持久化存储的标识符被修改(例如,通过更改元数据)
  • 持久化文件未通过 Coordinator 提供的迁移方式处理,而是采用了重新创建并手动复制数据的方式
  • 数据经过非轻量级迁移,导致对应的标识号(即 PK 值)发生变化

因此,为了更好地确保数据在选定数据或 Spotlight 等场景中能够正确匹配,开发者可以为数据添加自定义标识符属性,如 UUID。

只获取标识符以减少内存占用

在某些场景下,开发者并不需要立即访问所有检索到的数据,或只需使用其中一小部分。这时,可以选择仅获取符合检索条件的标识符,极大地减少内存占用。

在 Core Data 中,可以通过将 resultType 设置为 managedObjectIDResultType 来实现:

Swift
let request = NSFetchRequest<NSManagedObjectID>(entityName: "Item")
request.predicate = NSPredicate(format: "timestamp >= %@", Date() as CVarArg)
request.resultType = .managedObjectIDResultType
let ids = try? viewContext.fetch(request) ?? []

SwiftData 则为 ModelContext 提供了直接获取标识符的 fetchIdentifiers 方法:

Swift
func getIDS(_ ids: [PersistentIdentifier]) throws -> [PersistentIdentifier] {
  let now = Date()
  let predicate = #Predicate<Item> {
    $0.timestamp > now
  }
  let request = FetchDescriptor(predicate: predicate)
  let ids = try modelContext.fetchIdentifiers(request)
  return ids
}

通过这种方式,开发者可以在减少内存消耗的同时灵活获取所需数据。

总结

NSManagedObjectIDPersistentIdentifier 是 Core Data 和 SwiftData 中的核心概念和工具。深入理解并掌握它们的使用,不仅能帮助开发者更好地理解这些框架,还能有效提升代码的并发安全性和性能。

每周一晚,与全球开发者同步,掌握 Swift & SwiftUI 最新动向
可随时退订,干净无 spam