CoreData 探秘 – 从数据模型构建到托管对象实例

CoreData 探秘 – 从数据模型构建到托管对象实例

对每一个运用 Core Data 的开发者来说,用 Xcode 的 Core Data 模型修改器构建数据模型、创立容器、加载数据模型并经过保管目标上下文终究创立保管目标实例,这都是十分一般的进程。但你是否好奇过这一切的内部运行机制,Core Data 是如安在暗地辅佐咱们完结这一切的?本文将深化探求 Core Data 是怎么经过数据模型构建出保管目标实例的内部运行机制,读完本文能够让你对 Core Data 的作业流程有更深化的理解,在开发中能够更称心如意。

原文宣布在我的博客wwww.fatbobman.com

欢迎订阅我的大众号:【肘子的Swift记事本】

写在前面的话

最近我正在编撰有关 SwiftData 并发的文章。原计划在第一部分中评论 SwiftData 怎么依据模型声明来创立 PersistentModel 实例。本打算用几段文字阐明,但在写作时发现无法简单表述,有必要将该部分独立成文。当我着手编写这篇文章时,又发现需求另一篇文章来具体说明 Core Data 版别的完成进程。由此偶然间诞生了这篇文章。

在本文中,咱们不会深化评论从构建数据模型到创立保管目标实例的每个细节。咱们主要将评论两个环节:Core Data 怎么将模型文件转化为 ManagedObjectModel,以及它怎么从中提取信息来创立保管目标实例。

本文将以 Xcode 创立的 Core Data 项目模版供给的数据模型文件作为评论根底

用模型修改器构建 Core Data 数据模型文件

Xcode 的模型修改器为咱们供给了一个可视化的界面来定义 Core Data 运用程序的数据模型,包含实体、特点等信息。运用模型修改器能够更直观地构建数据模型。

当新建项目挑选包含 Core Data 时,Xcode 会在项目中主动创立一个名为 ProjectName.xcdatamodeld 的数据模型文件( Core Data Model Bundle )。咱们也能够自行在项目中创立 Core Data 数据模型文件,其文件扩展名为 .xcdatamodeld

CoreData 探秘 - 从数据模型构建到托管对象实例

CoreData 探秘 - 从数据模型构建到托管对象实例

Xcode 将开发者在模型修改器中创立的一切信息都保存在 xcdatamodeld 中。

切当来说,xcdatamodeld 是一个目录,通常被称为 “Core Data Model Bundle”。它是一个特殊的 Bundle,用于存储和办理 Core Data 的数据模型信息。它包含了一个或多个数据模型文件(.xcdatamodel)以及其他与数据模型相关的信息。Xcode 会在 xcdatamodeld 中为每个模型版别分别创立一个 VersionName.xcdatamodel 的 Bundle。

现在,用文本修改器打开 xcdatamodel 中的 content 文件,能够看到,当时版别的一切模型信息,都是以 XML 的格式保存在其间。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="false" userDefinedModelVersionIdentifier="">
    <entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
        <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
    </entity>
    <elements>
        <element name="Item" positionX="-63" positionY="-18" width="128" height="44"/>
    </elements>
</model>

其间,每个 entity 元素对应一个 Entity,包含了实体称号、对应的子类称号、特点、联系、自定义索引等很多信息。假如咱们在模型修改器中创立了新的 Configuration 或 Fetch Request ,也能在 XML 文件中找到对应的信息。在 Xcode 14 中,可视化的联系视图被取消了。这个联系视图在模型修改器中起到了重要的效果,能够直观地显示实体之间的联系。由于取消了可视化的联系视图,elements 元素中的信息基本上失去了效果。

Xcode 在编译项目时,会将 .xcdatamodel 目录以 momd 为尾椎增加到运用的资源中,其间的 xcdatamode Bundle 会编译成尾缀为 mom 的二进制文件,一方面削减空间占用,另一方面也能够进步加载速度。这也是当咱们用代码加载模型文件时,尾缀需求设置为 momd 的原因。

开发者应该了解的是,咱们经过 Xcode 的模型修改器创立的模型文件仅仅一种对模型的结构化表达,并非程序化表达。

生成实体对应的 NSManagedObject 子类声明

在绝大多树状况下,开发者都会为 Entity 创立对应的 NSManageObject 子类声明。当 Codegen 设置为 Class Definition 或 Category/Extension 时,Xcode 会隐式的帮咱们完结这项作业。

CoreData 探秘 - 从数据模型构建到托管对象实例

当 Codegen 设置为 Class Definition 时,Xcode 会生成一个独立的 NSManagedObject 子类,其间包含了实体特点和办法的定义。例如:

@objc(Item)
public class Item: NSManagedObject {}
extension Item {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Item> {
        return NSFetchRequest<Item>(entityName: "Item")
    }
    @NSManaged public var timestamp: Date?
}
extension Item : Identifiable {}

当 Codegen 设置为 Category/Extension 时,Xcode 会生成一个扩展,将实体特点和办法增加到 NSManagedObject 的默许完成中。这样能够防止修改主动生成的代码,保持代码的可保护性。

@NSManaged 是一个特点修饰符,用于标记一个被 Core Data 办理的特点。它告诉编译器这个特点将由 Core Data 主动生成相关的存取办法,并且在运行时会动态地与保管目标上的特点进行相关。

开发者也能够挑选手动创立这些代码,或运用 Xcode 显式生成。手动创立代码能够更准确地表达特点类型,并且灵活性更高。运用 Xcode 生成代码能够省去手动编写的作业量,特别是在特点较多或模型结构杂乱的状况下。不管挑选哪种办法,生成一个契合 NSManagedObject 的子类声明,能够让开发者愈加安全、方便地拜访保管目标的保管特点,并且经过重写子类的某些办法(例如:willSave),能够将某些操作特定到具体的实体上。

extension Item {
    public override func willSave() {
        super.willSave()
        // do something
    }
}

尽管能够获得上述优势,但为实体声明一个对应的 NSManagedObject 的子类并非是有必要的。这是由于 Core Data 也供给了一种轻量级的办法来拜访和操作保管目标,即运用 NSManagedObject 目标自身来进行特点拜访和操作。

// item:Item
let timestamp = item.timestamp
// object is a NSManagedObject instance create by Item Entity description
let timestamp = object.value(forKey: "timestamp") // trigger KVO
let timestamp = object..primitiveValue(forKey: "timestamp") // not trigger KVO

在上面的示例中,item.timestamp 是经过为实体 Item 声明一个对应的 NSManagedObject 的子类( Item)来完成的,而 object.value(forKey:)object.primitiveValue(forKey:) 是经过 NSManagedObject 目标自身来拜访特点的办法。需求留意的是,value(forKey:) 办法会触发 Key-Value Observing (KVO),而 primitiveValue(forKey:) 办法则不会触发 KVO。

在某种程度上,咱们能够将 @NSManaged 视作与 Swift 的计算特点类似的机制。经过 value(forKey:)setValue(_:forKey:) 办法,咱们能够读取和设置保管目标的底层值。这使得咱们能够在需求的时分对特点进行自定义的逻辑操作,例如数据格式转化、数据校验等。

加载数据模型,创立 Container

自从 Core Data 供给了 NSPersistentContainer 后,除非特别状况,开发者简直都不会在代码中显式地读取数据模型文件并创立数据模型了( NSManageObjectModel )。

let container = NSPersistentContainer(name: "ModelEditorDemo")

但是,了解 Core Data 在创立 container 的背后所做的作业,对于之后理解保管目标实例的创立进程仍然十分有协助。

// Load the data model file and create NSManagedObjectModel
guard let url = Bundle.main.url(forResource: "ModelEditorDemo", withExtension: "momd"),
      let dataModel = NSManagedObjectModel(contentsOf: url) else {
     fatalError("Failed to load the data model file")
}
// Create the persistent store coordinator
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: dataModel)
// Get the configuration from the data model
let configuration = dataModel.configurations.first!
// Create the URL for the persistent store
let storeURL = URL.applicationDirectory.appending(path: "store.sqlite")
// Create or load the persistent store with the specified configuration
guard let store = try? coordinator.addPersistentStore(type: .sqlite, configuration: configuration,at: storeURL,options: nil) else {
    fatalError("Failed to create persistent store: \(error)")
}
// Create a main queue NSMangedObjectContext
let viewContext = NSManagedObjectContext(.mainQueue)
// Link context to coordinator
viewContext.persistentStoreCoordinator = coordinator

大致的流程如下:

  1. 获取数据模型文件(momd)的 URL。

  2. 运用该 URL 创立一个 NSManagedObjectModel 实例。

  3. 运用 NSManagedObjectModel 实例创立一个 NSPersistentStoreCoordinator 实例。

  4. 在 NSPersistentStoreCoordinator 实例上增加一个耐久化存储。

  5. 创立一个主线程的保管目标上下文。

  6. 将上下文与 NSPersistentStoreCoordinator 实例相关。

其间,在运用数据模型文件 URL 来创立 NSManagedObjectModel 实例的时分,Core Data 会将模型文件中的描绘率先转化成对实体的程序式表达,然后再经过这些程序式表达创立 NSManagedObjectModel 实例。这种转化进程使得咱们能够以编程办法来创立和操作数据模型,而不仅限于运用可视化修改器。

以编程的办法来描绘实体,创立数据模型实例

除了运用数据模型修改器进行可视化操作外,Core Data 供给了以编程的办法来表述实体并创立数据模型的办法。

下面的代码,展示了编程化的办法来描绘 Item 实体并创立数据模型的进程。

func createModel() -> NSManagedObjectModel {
    let itemEntityDescription = NSEntityDescription()
    // Entity Name
    itemEntityDescription.name = "Item"
    // NSManagedObject SubClass Name
    itemEntityDescription.managedObjectClassName = "Item"
    // Descriptor timestamp attribute
    let timestampAttribute = NSAttributeDescription()
    // Attribute Name
    timestampAttribute.name = "timestamp"
    // Is Optional
    timestampAttribute.isOptional = true
    // Attribute Type
    if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) {
        timestampAttribute.type = .date
    } else {
        timestampAttribute.attributeType = .dateAttributeType
    }
    // Add timestamp to Item
    itemEntityDescription.properties.append(timestampAttribute)
    // Create a empty NSManagedObject
    let model = NSManagedObjectModel()
    // Add Item Entity into model
    model.entities = [itemEntityDescription]
    return model
}

上面的代码简直与咱们在模型修改器中所做的操作一一对应。但是,当特点数量很多或联系杂乱时,可视化操作愈加高效和便利。经过可视化操作,咱们能够直观地在图形界面中增加、修改和删去实体、特点和联系,而不需求手动编写大量的代码。这使得数据模型的创立和保护变得愈加容易和快速。

现在咱们就能够用这段代码,替换掉之前经过数据模型文件创立 NSManagedObjectModel 的操作。

// Create data model by programming way
let dataModel = createModel()
// Create persistent store coordinator by dataModel
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: dataModel)

尽管可视化修改愈加高效,不过编程式表述为开发者供给更宽广的数据模型描绘的空间,能够将自定义的描绘办法映射为 Core Data 能够接受的程序化表达。这种灵活性使得开发者能够更好地满意特定的业务需求,别的,编程办法还能够供给更多的类型安全和编译时检查,削减了在运行时呈现过错的或许性。

创立保管目标实例

Core Data 是一个目标图办理结构,咱们构建数据模型的意图是为了以面向目标的办法操作耐久化数据。具体的数据操作通常会在保管目标实例上进行。

最常见的获取保管目标实例的途径有两种:

  • 设置谓词,经过 NSFetchRequest ,Core Data 将契合条件数据以保管目标的方式返回给开发者
  • 经过直接调用与 Entity 对应的 NSManagedObject 子类的结构办法创立保管目标实例

开发者惯常运用下面这种办法创立保管目标实例:

let item = Item(context: viewContext)
item.timestamp = .now
try? viewContext.save()

但是 init(context:) 要求咱们有必要首先创立保管目标上下文( NSManagedObjectContext ),其实,在 Core Data 中,咱们完全能够在没有上下文的状况下来创立保管目标实例。

let item = Item(entity: Item.entity(), insertInto: nil)
item.timestamp = .now
viewContext.insert(item)
try? viewContext.save()

事实上,init(entity:, insertInto:) 结构办法是 NSManagedObject 的指定初始化器(designated initializer),而 init(context:) 是其便捷初始化器(convenience initializer)。创立保管目标实例的关键并不在于是否有保管目标上下文,而在于告诉 NSManagedObject,该实例对应的是哪个 EntityDescription。

需求留意的是,当咱们运用 Item.entity() 来获取 Item 对应的 EntityDescription 时,需确保 NSManagedObjectModel 现已被 NSPersistentStoreCoordinator 加载。

let coordinator = NSPersistentStoreCoordinator(managedObjectModel: dataModel)

在 Core Data 中,当 NSPersistentStoreCoordinator 被创立后,数据模型会被保存在一个可供内部元素拜访的方位,以便获取。Item.entity() 办法会从中获取 Item 对应的 EntityDescription。假如咱们在创立 NSPersistentStoreCoordinator 时没有运用包含 Item 的数据模型,或底子没有创立 NSPersistentStoreCoordinator,调用 Item.entity() 后,Core Data 会抛出如下过错:

CoreData: error: No NSEntityDescriptions in any model claim the NSManagedObject subclass 'Item' so +entity is confused.  Have you loaded your NSManagedObjectModel yet ?

这并不意味着咱们没有其他办法能够绕过 NSPersistentStoreCoordinator 的约束。

guard let url = Bundle.main.url(forResource: "ModelEditorDemo", withExtension: "momd"),
      let dataModel = NSManagedObjectModel(contentsOf: url) else {
     fatalError("Failed to load the data model file")
}
let entityDescription = dataModel.entitiesByName["Item"]!
let item = Item(entity: entityDescription, insertInto: nil)

经过直接从 NSManagedObjectModel 获取对应的 EntityDescription,开发者能够在仅拥有 NSManagedObjectModel 实例的状况下,就具备了创立保管目标实例的条件。这对于某些特定状况下,只需求操作数据模型而无需操作保管目标上下文的场景十分有用。

阅读 如安在 Xcode 下预览含有 Core Data 元素的 SwiftUI 视图 一文,检查此种办法在 SwiftUI 预览中的运用。

正如前文所提到的,开发者并不一定要创立保管目标子类的实例。经过运用正确的 EntityDescription,咱们能够创立 NSManagedObject 实例,在许多场景下能够到达同样的效果。

let item = NSManagedObject(entity: Item.entity(), insertInto: nil)
item.setValue(Date.now, forKey: "timestamp")
viewContext.insert(item)
try? viewContext.save()

最后

在本文中,咱们评论了几种不同的在 Core Data 中构建数据模型和创立保管目标实例的办法,其间一些办法或许并不常见。有些读者或许会对这些办法感到困惑,但即使不了解这些办法,也不会影响咱们熟练运用 Core Data。但是,本文创造的意图正是向读者介绍这些十分见的办法,由于在接下来的文章中,咱们将评论 “SwiftData 怎么依据模型声明来创立 PersistentModel 实例”。到时,咱们将看到 SwiftData 开发团队是怎么使用本文介绍的内容和 Swift 的新特性,构建出契合新时代的耐久化结构的。

欢迎你经过 Twitter、 Discord 频道 或博客的留言板与我进行交流。

订阅下方的 邮件列表,能够及时获得每周最新文章。

原文宣布在我的博客wwww.fatbobman.com

欢迎订阅我的大众号:【肘子的Swift记事本】