核心数据定律

2020-12-22 13:43:48

在与开发人员的对话中,我听到了一个非常普遍的主题,即“核心数据很难”或“核心数据有错误”或“我永远无法使它正常工作并放弃”。

我花了很多时间使用Core Data,并以为我会分享“ Laws of Core Data”。这些是我随着时间的流逝而制定的一套规则,这些规则几乎完全不费力地使用Core Data。当我遵循这些规则时,使用它几乎没有任何问题。

经常听到开发人员谈论核心数据并将其视为数据库的情况。他们看到它由SQLite提供支持,并认为它在功能上等效。

核心数据是一个“对象图和持久性框架”,基本上就像是一种奇特的对象关系映射。这就是一堆完整的代码,可以帮助您维护对象的图形(即具有定义的组织的相关数据的“网络”),然后以某种方式持久化它们。

这并不一定意味着您具有包含数据行的表。这并不一定意味着您具有跨数据类型进行联接的能力。这不一定意味着它甚至已作为文件存储在磁盘上。

拥有这些功能意味着与使用传统数据库相比,您可以让Core Data为您处理更多的逻辑。

这与第一定律有很大关系,但是更具体一些,它与Core Data持久化数据的方式有关。很少会发现没有使用SQLite作为持久层的Core Data实现,但是确实发生了。

除了这些,Core Data还允许您通过子类化NSAtomicStore或NSIncrementalStore来创建自己的持久性机制。因此,如果您愿意,可以使Core Data将内容保存到git存储库,CloudKit,MySQL或PostgreSQL或您自己的自定义后端中。...几年前,我创建了一个框架来访问stackoverflow.com API,联网是通过自定义核心数据存储完成的,该存储将核心数据请求转换为API调用。这很奇怪,但是有效。

核心数据并不仅限于SQLite。实际上,对架构进行建模就好像它是SQLite(或其他RDBMS变体)一样,这无疑是您“做错了”的标志。几乎不需要设置诸如人造外键或联接表之类的自定义内容,并且几乎总是错误的。

通常,开发人员在创建Core Data堆栈时要做的第一件事就是创建一个“ DataStack”对象,该对象封装了加载模型,创建存储协调器,然后创建主NSManagedObjectContext的过程。然后,该“堆栈”对象将作为“核心数据管理器”对象传递,通过它可以获取所需的上下文。 iOS 10.0和macOS 10.12添加了NSPersistentContainer的概念,它可以为您完成很多工作。

拥有一个对象即可加载模型,一切都很好。但是您不需要传递它。

为了方便访问新的上下文或访问模型,通常将其传递出去。那都是不必要的。如果您确实决定在应用程序中传递Core Data对象,那么您只需要NSManagedObjectContext(“ MOC”)。

您的MOC具有NSPersistentStoreCoordinator(“ PSC”)属性,该属性本身具有NSManagedObjectModel(“ MOM”,也称为架构)。因此,从单个MOC中,您可以获得与架构有关的任何信息,存储内容的位置,存储格式,持久性存储的配置等。

如果您决定需要创建一个新的一次性MOC,则可以使用现有的MOC轻松做到这一点:

让existingContext:NSManagedObjectContext = ...让newContext = NSManagedObjectContext(concurrencyType:.privateQueueConcurrencyType)newContext.persistentStoreCoordinator = existingContext.persistentStoreCoordinator //做到这一点

(创建新的环境并不理想,因为还有其他法律规定)

涉及核心数据时,此法律是错误的来源。随便说一句,我想开发人员在使用Core Data时遇到的痛苦有90%以上是因为这个。

核心数据试图提高效率;它通常不希望加载比您需要的数据更多的数据,这意味着有时您会要求它提供数据(例如对象属性),并且它不方便使用。发生这种情况时,它必须先从其存储中加载数据(甚至可能不是磁盘上的本地文件!),然后它才能响应您。

这称为“故障”。被管理对象保留的内部标记值是“故障”,而“完成”(即,检索数据)故障的过程就是“故障”。

关键在于:核心数据必须安全。它必须将这些错误的调用与持久性存储的其他访问进行同步,并且必须以不干扰其他数据错误调用的方式进行。这样做的方法是期望所有对数据错误的调用都在其队列之一中安全地发生。

每个受管理对象“都属于”特定的MOC(稍后将对此进行详细介绍),并且每个MOC都有一个DispatchQueue,可用于同步其有关从其persistentStoreCoordinator加载数据的内部逻辑。

如果您是在MOC队列之外使用NSManagedObject的,那么对数据故障的调用将无法正确同步和受到保护,这意味着您很容易出现竞争状况。

因此,如果您有一个NSManagedObject,则唯一安全的使用位置是在其MOC上对perform或performAndWait的调用中,如下所示:

let对象:NSManagedObject = ... var propertyValue:PropertyType! object.managedObjectContext.performAndWait {propertyValue = object.property} ...

使用您自己的DispatchQueue或全局队列之一是不够的。必须从MOC控制的队列中访问托管对象,而执行此操作的方法是使用perform和performAndWait方法。

这有一个特殊情况,那就是处理属于MOC的托管对象,该MOC的队列为“主”队列。 DispatchQueue.main队列绑定到应用程序的主线程,因此,如果您在主线程上并且具有主线程对象,则可以“安全地”不使用执行调用,因为您已经在上下文的内部队列。

可以在队列外使用或在队列/线程之间传递的唯一安全托管对象属性是对象的objectID:这是Core Data提供的该特定对象唯一的标识符。您可以从任何地方访问此属性,这是将托管对象从一个上下文“转移”到另一个上下文的唯一方法:

let objectInContextA:NSManagedObject = ... let objectID = objectInContextA.objectID let contextB:NSManagedObjectContext = ... contextB.perform {let objectInContextB = contextB.object(with:objectID)// objectInContextB现在是与原始对象分开的* instance *对象,//但都由持久性存储中的相同数据支持}

我要在这里补充一点,很遗憾,我们必须对此予以关注。不难想象,托管对象会自动处理这种事情。但是,当我们处理的框架已经使用了14年以上,并且基于24年的另一个框架(EOF)时,就会发生这种情况。 “二进制兼容性”问题是另一天的博客文章。

这是先前法律的概括。由于错误和队列访问的怪异,我认为NSManagedObject实际上不应该是NSObject的子类。当我们在代码中看到NSObject时,就对它们如何在内存管理,多线程访问和行为方面进行了假设。 NSManagedObject违反了这些规则,因此它可能不应该是NSObject,而应该是其自己的根类。

因此,忘记它是一个NSObject。它的行为实际上并不像一个,并且您不应像使用它那样使用它。

核心数据的一项更深奥的功能是能够在上下文之间建立关系:您可以拥有一个MOC,该MOC实际上不受NSPersistentStoreCoordinator支持,而由另一个MOC支持。这确实有一些有趣的含义,但总的来说:您不需要。

在某些特殊情况下,拥有孩子MOC的能力很巧妙。让我们回顾一下MOC的核心功能,以了解这些情况:

那才是真正的核心。因此,如果满足以下条件,您将需要一个儿童MOC:

如您所见,当处理子上下文时,您实际上是在处理临时(非持久)对象。您将从根本上改变加载和保存行为。

需要这种情况的时间很少见。对于复杂的子图创建流程,您通常会希望这样做,在流程的每个步骤中,您都需要加强关系的完整性,但是在流程完成之前,您不希望将其实际保存到持久性存储中。而且,如果取消了流程,您根本就不会保存任何流程。您可以通过具有子上下文,在子上下文中执行所有流程步骤并将其保存到父上下文中来实现,但是如果用户中止,您仍然可以删除子上下文。

它们有点像普通数据库系统中的事务。您可以开始导入或编辑大量数据,如果出现问题或被取消,则可以回滚更改。

通常提倡父/子上下文,例如“在后台加载一些数据,并将其保存到主队列上下文中”。可以奏效,但这确实意味着要保留数据,实际上必须保存两个上下文,而不是仅保存一个上下文(因为save()-上下文只能将数据上移一个级别。对于子上下文,数据仅到达父上下文,而不是一直到PSC)。我认为,使用这样的儿童上下文会不必要地变得复杂。

对于一般的非事务性用法,我认为最好有两个直接链接到PSC的上下文(一个用于主线程,一个用于后台)。数据的导入是在后台上下文中完成的,并且在保存时,主队列侦听NSManagedObjectContextDidSave通知,并使用.mergeChanges(fromContextDidSave :)方法合并更改以更新其内部保存的对象。如果上下文自动将MergesChangesFromParent设置为true,那么即使是该步骤也可能是不必要的。

如果您要构建的应用程序正在从Core Data中读取信息,将其显示给用户并允许进行最少的编辑,那么根据我的经验,最好将主队列上下文保持为“只读”上下文。

通过具有关于可读性和可写性的严格的规则,可以更轻松地推断何时应重新加载部分UI:用于更新UI的命令来自单个方向(从模型到UI)。如果您允许对存储信息进行突变,则可以将其封装为一种“突变请求”,发送给模型这一部分的控制器,然后在此处执行。直接在Core Data对象上执行突变使您更难以调试更改的来源(数据导入步骤,在UI中进行编辑或其他操作),因为您只有一个入口点。

如果您也遵循下一条法律,那么执行该法律将变得非常简单。

这更像是“一般性好的建议”,而不是针对核心数据的任何建议,但实际上是:

从应用程序的其余部分隐藏您正在使用Core Data的事实通常是明智的。这并不是因为您对此感到“羞愧”并需要使其模糊不清(😉),而是因为Core Data对象随身携带了相当数量的行李,而应用程序的其余部分则不应必须了解这一点(请参阅前面的有关对象如何带入整个堆栈的知识)。

当您在应用程序周围传递托管对象或上下文时,仅进入对象内部并拉出PSC或MOM或其他任何东西并使用它的诱惑就会变得过高。不要那样做避免违反Demeter法则,并拥有适当的控制器对象,您可以索要您需要的东西。

您可以将托管对象隐藏在协议后面,但是这也很容易忘记有关队列使用的法律。

我认为,应将图形完整性和持久性的详细信息保留在应用程序的受限部分,并且数据应仅通过自定义结构值(或类似的值)输出。

作为一个大概的例子,您可以执行以下操作:

协议ManagedObjectInitializable {init(managedObject:NSManagedObject)}类ModelController {func fetchObjects< T>(完成:@escaping(Array< T>)->无效)其中T:ManagedObjectInitializable {...}}结构体:ManagedObjectInitializable {让firstName:字符串let lastName:字符串...}

您可以采用多种不同的方法来提取核心数据的详细信息,每种方法各有利弊。但是从应用程序的其余部分隐藏这样的Core Data是实现正确封装和“需要知道”信息隐藏的巨大一步。

如今,使用在设备之间同步数据或由服务器后端提供支持的应用程序已经很普遍。在极少数情况下,您找不到会产生和使用仅设备本地数据的应用程序。

因此,我发现将Core Data用作本地缓存非常好。由于Core Data隐藏在抽象层的后面,因此可以轻松地集成到我从中请求数据的模型层中。模型层在核心数据中查找,如果有数据,则返回该数据。如果不是,则将获取数据,将其保存到Core Data中,然后返回。

以这种方式使用Core Data意味着,如果我遇到架构冲突(即,我已使用新的架构版本更新了我的应用,并且旧版本中的持久数据不再与新版本兼容),我不会实际上,对于否定整个持久性存储并重新开始没有任何疑虑。当然,我可以执行迁移过程并处理手动改组数据以采用新格式的过程,但是我不必这样做。那是巨大的,为我节省了大量的工作。

但是,这种方法有一个很大的“陷阱”:当您要查询整个数据集时,核心数据最有效。由于Core Data非常关注验证和图形完整性,因此如果丢失部分数据,它就不能像缓存一样有效。您可以在模式中考虑到这一点,但是这也会使您的使用方式复杂化。因此,如果您打算将Core Data用作本地缓存,则最好将其用于整个数据集。

因此,这些是我使用Core Data的“法律”。当我遵循这些法律时,我几乎不会遇到种族状况,数据损坏,“悲伤如乐观主义之死”或数据完整性等问题。这一切都可以正常工作,并且往往可以非常非常好地工作。

特别感谢Cole Joplin,Tom Harrington和Soroush Khanlou的校对和提供反馈。