Core Data 和 SwiftData 是苹果为开发者设计的强大数据管理框架,能够高效处理复杂的对象关系,因而被称为对象图管理框架。在这两个框架中,NSManagedObjectID
和 PersistentIdentifier
功能相似,且都极为重要。本文将深入探讨它们的功能、使用方法及注意事项。
什么是 NSManagedObjectID 和 PersistentIdentifier?
在 Core Data 和 SwiftData 中,NSManagedObjectID
和 PersistentIdentifier
分别作为数据对象的“身份证”,用于让系统能够在持久化存储中准确找到相应的记录。它们的主要功能是帮助应用程序在不同的上下文和生命周期内,正确识别和管理数据对象。
在 Core Data 中,托管对象的 objectID
属性可以获取对应的 NSManagedObjectID
:
let item = Item(context: viewContext)
item.timestamp = Date.now
try? context.save()
let id = item.objectID // NSManagedObjectID
而在 SwiftData 中,可以通过数据对象的 id
或 persistentModelID
属性获取相应的 PersistentIdentifier
:
let item = Item(timestamp: Date.now)
modelContext.insert(item)
try? modelContext.save()
let id = item.persistentModelID // PersistentIdentifier
值得注意的是,Core Data 中 NSManagedObject
的默认 id
属性实际上是 ObjectIdentifier
,而非 NSManagedObjectID
。如果需要,可以通过扩展重新声明该属性,将其修改为 NSManagedObjectID
:
public extension NSManagedObject {
var id: NSManagedObjectID { objectID }
}
为了简化后文,除非特别需要区分,我将使用“标识符”作为
NSManagedObjectID
和PersistentIdentifier
的统称。
临时 ID 与永久 ID
在 Core Data 和 SwiftData 中,当一个数据对象刚创建且尚未持久化时,其标识符为临时状态。临时标识符无法在跨上下文中使用,即无法在另一个上下文中获取对应的数据。
在 Core Data 中,NSManagedObjectID
提供了 isTemporaryID
属性,用来判断标识符是否为临时状态:
let item = Item(context: viewContext)
item.timestamp = Date.now
// 数据未保存
print(item.objectID.isTemporaryID) // true
然而,在 SwiftData 中,当前并没有类似的属性或方法可以直接判断 PersistentIdentifier
的状态。由于 SwiftData 的 mainContext
默认启用了 autoSave
功能(开发者无需显式保存数据),因此在创建数据对象后,标识符可能暂时无法在其他上下文中使用。如果遇到这种情况,可以通过手动显式保存来避免该问题。
标识符与持久化数据的对应关系
永久 ID(即持久化后的标识符)中包含了足够的信息,框架可以依赖这些信息在数据库中定位到相应的数据。当我们打印一个永久 ID 时,可以看到其详细内容:
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,未保存的对象会缺少表中的标识号,如下所示:
item.objectID.isTemporaryID // true, 临时 ID
print(item.objectID)
// x-coredata:///Item/t6E5D1507-3E60-41F0-A5F7-C1F28DC63F402
SwiftData 的默认实现仍基于 Core Data,因此 PersistentIdentifier
的格式与 NSManagedObjectID
十分相似:
print(item.persistentModelID)
// SwiftData.PersistentIdentifier(id: SwiftData.PersistentIdentifier.ID(url: x-coredata://A07B3AB6-F28D-4F15-9B5D-9B12EB052BC6/Item/p1), implementation: SwiftData.PersistentIdentifierImplementation)
当 PersistentIdentifier
处于临时状态时,它同样缺少表中的标识号:
// 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
:
public struct PersistentIdentifier : Hashable, Identifiable, Equatable, Comparable, Codable, Sendable
NSManagedObjectID
作为 NSObject
的子类,早期并没有明确的线程安全标注。
在 Ask Apple 2022 中,苹果工程师确认它是线程安全的,开发者可以使用 @unchecked Sendable
进行标注。从 Xcode 16 开始,Core Data 框架已经对此进行了官方标注,开发者无需再手动标注。
open class NSManagedObjectID : NSObject, NSCopying, @unchecked Sendable
因此,标识符在 Core Data 和 SwiftData 中是确保安全并发操作的关键。
请阅读 SwiftData 中的并发编程 和 Core Data 并发编程提示 了解更多有关并发操作的内容。
如何通过标识符获取对应的数据
获取数据的方法主要可以分为两类:基于谓词的查询和使用上下文或 ModelActor
提供的直接方法。
基于谓词
在 Core Data 中,可以通过构建谓词,根据 NSManagedObjectID
来检索数据:
// 根据单个 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 中,同样可以通过类似的方式构建谓词:
// 根据单个 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 中,虽然 PersistentModel
的 id
属性也是 PersistentIdentifier
,但在谓词中只能使用 persistentModelID
进行检索。
使用上下文获取单个数据
Core Data 的 NSManagedObjectContext
提供了三种不同的方式来根据 NSManagedObjectID
获取数据,区别如下:
-
existingObject(with:)
如果上下文中已存在指定对象,该方法会返回该对象;否则,它会从持久化存储中获取并返回完整实例化的对象。与
object(with:)
不同,它不会返回一个未初始化的对象。如果对象既不在上下文也不在存储中,会抛出错误。换句话说,只要数据存在,该方法必定返回完整对象。Swiftfunc 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 实例的下标方法实现:
@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
可能依赖这些私有属性来检索数据,而不仅仅是通过持久化存储的标识符来定位。
如何持久化标识符
为了安全地跨协调器使用标识符并实现其持久化,可以通过 NSManagedObjectID
的 uriRepresentation
方法生成只包含“持久化存储 ID + 表名 + 标识号”的 URI。持久化后的 URL 不仅可以跨协调器使用,即使在应用冷启动后,也能通过该 URL 恢复正确的数据。
let url = item.objectID.uriRepresentation()
对于 PersistentIdentifier
,由于其遵循了 Codable
协议,最便捷的持久化方式是将其编码并保存:
let id = item.persistentModelID
let data = try! JSONEncoder().encode(id)
标识符的持久化在多个场景下都很有用。例如,当用户选择某个数据时,持久化该数据的标识符可以在应用冷启动后恢复到退出时的状态。或者在使用 Core Spotlight 框架时,可以将标识符添加到 CSSearchableItemAttributeSet
中,用户通过 Spotlight 搜索到数据后,可以直接通过标识符跳转到对应的数据视图。
更多信息请参阅 在 Spotlight 中展示应用中的 Core Data 数据。
如何创建标识符
在 Core Data 中,虽然 NSManagedObjectID
没有公开的构造方法,但我们可以通过有效的 URL 来生成相应的 NSManagedObjectID
实例:
let container = persistenceController.container
if let objectID = container.persistentStoreCoordinator.managedObjectID(forURIRepresentation: uri) {
let item = getItem(id: objectID)
}
在 iOS 18 中,Core Data 引入了一个直接通过字符串构建标识符的新方法:
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
:
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
的方法,展示了标识符的组成要素:
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)的标识符,主要用于自定义存储实现。但我们可以利用此思路创建一个支持默认存储的标识符构造方法:
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
来实现:
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
方法:
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
}
通过这种方式,开发者可以在减少内存消耗的同时灵活获取所需数据。
总结
NSManagedObjectID
和 PersistentIdentifier
是 Core Data 和 SwiftData 中的核心概念和工具。深入理解并掌握它们的使用,不仅能帮助开发者更好地理解这些框架,还能有效提升代码的并发安全性和性能。