核心数据详细

最近,我开始使用Core Data进行大型项目。 通常的事情是项目人员会改变,经验会丢失,而细微之处会被遗忘。 不可能使每个人都深入研究特定的框架-每个人都有自己的工作问题。 因此,我准备了一个简短的演讲,从我认为很重要或在教程中没有涵盖的观点出发。 我与所有人共享,希望这将有助于编写有效的代码而不犯错误。 假设您已经对这个主题有些了解。

我将从平庸开始。

核心数据是在应用程序中管理和存储数据的框架。 您可以将Core Data视为物理关系存储的包装,该物理关系存储以对象的形式表示数据,而Core Data本身不是数据库。

核心数据对象


图片

为了创建存储,应用程序使用NSPersistentStoreCoordinatorNSPersistentContainer类 。 NSPersistentStoreCoordinator基于模型创建指定类型的存储,您可以指定位置和其他选项。 NSPersistentContainer可以与iOS10一起使用,提供以最少的代码创建的能力。

它的工作方式如下:如果指定路径上存在数据库,则协调器检查其版本,并在必要时进行迁移。 如果数据库不存在,则会根据NSManagedObjectModel模型创建数据库。 为了使所有这些正常工作,在更改模型之前,请通过菜单编辑器->添加模型版本在Xcode中创建新版本。 如果您获得路径,则可以在仿真器中找到并打开基础。

NSPersistentStoreCoordinator的示例
var persistentCoordinator: NSPersistentStoreCoordinator = { let modelURL = Bundle.main.url(forResource: "Test", withExtension: "momd") let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL!) let persistentCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel!) let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] let storeURL = URL(fileURLWithPath: documentsPath.appending("/Test.sqlite")) print("storeUrl = \(storeURL)") do { try persistentCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: [NSSQLitePragmasOption: ["journal_mode":"MEMORY"]]) return persistentCoordinator } catch { abort() } } () 

NSPersistentContainer示例
 var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "CoreDataTest") container.loadPersistentStores(completionHandler: { (storeDescription, error) in print("storeDescription = \(storeDescription)") if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container } () 


核心数据使用4种类型的存储:

-SQLite
-二进制
-内存中
-XML(仅Mac OS)

例如,出于安全原因,如果您不想以文件形式存储数据,但同时又希望在会话期间使用缓存并且以对象形式使用数据,则“内存中”类型的存储非常适合。 实际上,在一个应用程序中禁止具有几种不同类型的存储。

我想谈谈NSManagedObjectContext对象。 通常,Apple为NSManagedObjectContext(用于处理Core Data对象的环境)提供了非常模糊的措辞。 所有这些都是出于将自己从与关系数据库的关联中分离出来,以及将核心数据呈现为一种易于使用的工具的需求,该工具不需要了解键,事务和其他Bazdan属性。 但是在关系数据库的语言中,NSManagedObjectContext在某种意义上可以称为事务管理器。 您可能已经注意到它具有保存和回滚方法,尽管很可能仅使用第一个方法。

对这个简单事实的误解导致使用单上下文模型,即使在这种情况下,这仍然是不够的。 例如,您正在编辑一个大文档,而同时您必须下载几个目录。 您什么时候叫保存? 如果我们使用的是关系数据库,那么毫无疑问,因为每个操作都将在其自己的事务中执行。 核心数据还有一个非常方便的方法来解决此问题-这是子上下文的一个分支。 但不幸的是,由于某种原因,这种方式很少使用。 这里有一篇关于该主题的好文章

传承


由于某种原因,我不明白,有大量的手册和示例都没有使用Entity / NSManagedObject(表)的继承。 同时,这是一个非常方便的工具。 如果不使用继承,则只能通过KVC机制为属性(字段)分配值,该机制不检查属性的名称和类型,这很容易导致运行时错误。

NSManagedObject的类重新定义在Core Data设计器中完成:

图片

继承和代码生成


在为Entity指定类名之后,您可以使用代码生成并使用现成的代码获取一个类:

图片

图片

如果要查看自动生成的代码,但不想将文件添加到项目中,则可以使用另一种方法:为Entity设置“ Codegen”选项。 在这种情况下,必须在../ DerivedData / ...中搜索代码。

图片

使用代码生成来创建类,变量名称中的拼写错误可能导致运行时错误。

这是一些这样的代码:

 @objc public class Company: NSManagedObject {    @NSManaged public var inn: String?  @NSManaged public var name: String?  @NSManaged public var uid: String?  @NSManaged public var employee: NSSet? } 

快速地,@ NSManaged与Objective C中的dynamic具有相同的含义。
核心数据本身负责接收其类属性的数据(具有内部访问器)。 如果您有运输字段,则需要添加函数来计算它们。

与其他类不同,从IOS10之前继承自NSManagedObject(表)的类没有“常规”构造函数。 为了创建Company类型的对象,有必要使用NSEntityDescription编写一个笨拙的构造。 现在,有了通过上下文初始化的更方便的方法(NSManagedObjectContext)。 代码如下。 注意在通过KVC机制分配属性时继承的优点:

 // 1 -    NSEntityDescription,    KVO let company1 = NSEntityDescription.insertNewObject(forEntityName: "Company", into: moc) company1.setValue("077456789111", forKey: "inn") company1.setValue(" ", forKey: "name") // 2 -    NSEntityDescription,     let company2 = NSEntityDescription.insertNewObject(forEntityName: "Company", into: moc) as! Company company2.inn = "077456789222" company2.name = " " // 3 -     (IOS10+),     let company3 = Company(context: moc) company3.inn = "077456789222" company3.name = " " 

NSManagedObject的命名空间


值得一提的另一件事是名称空间。

图片

如果您在ObjectiveC或Swift上工作,您将没有任何困难。 通常,默认情况下正确填写此字段。 但是在混合项目中,对于swift和ObjectiveC的类,您需要放下不同的选项,这可能会让您感到惊讶。 在Swift中,必须填写“模块”。 如果此字段未完成,则将带有项目名称的前缀添加到类名称,这将导致运行时错误。 在Objetive C中,将“模块”留空,否则在通过类名称访问它时将找不到NSManagedObject。

对象之间的链接


原则上,关系的主题涵盖了很多,但我想重点介绍如何将子实体添加到父实体。 因此,首先,快速提醒一下创建链接的机制。 考虑一个传统的例子,公司是员工,联系是一对多的:

  • 在每侧(表)上创建连接
  • 之后,“反向”字段变为可用,必须在每个表中将其填充。

图片

图片

苹果公司坚持规定逆关系。 同时,反转并不能增强一致性,但是可以帮助Core Data跟踪连接两侧的变化,这对于缓存和更新信息非常重要。

正确指定删除规则也很重要。 删除规则是在删除父对象时将对此对象执行的操作。

  • 级联-删除所有父对象时删除所有子对象。
  • 拒绝-禁止删除有孩子的父母
  • Nullify-使父引用无效
  • 无动作-未指定任何动作,编译时会发出警告

在此示例中,删除公司后,所有员工都将被删除(级联)。 删除员工后,公司中与该员工的链接将重置(预屏)

将子实体添加到父实体的方法


1)第一种方法是通过NSSet添加。 例如,向公司添加2名员工:

 let set = NSMutableSet();    if let employee1 = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: moc) as? Employee { employee1.firstName = "" employee1.secondName = "" set.add(employee1) } if let emploee2 = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: moc) as? Employee { employee2.firstName = "" employee2.secondName = "" set.add(employee2) }    company.employee = set 

此方法便于初始化对象或填充数据库。 有细微差别。 如果公司已经有员工,并且您分配了新的员工,则以前的员工将重置与公司的链接,但不会删除它们。 或者,您可以获取员工列表,并已使用此集合进行工作。

 let set = company.mutableSetValue(forKey: "employee") 

2)通过父ID添加子对象

 if let employee = NSEntityDescription.insertNewObject(forEntityName: "Employee", into: moc) as? Employee { employee.firstName = "" employee.secondName = "" employee.company = company } 

第二种方法在添加或编辑子对象时很方便
单独的形式。

3)通过自动生成的方法添加子对象

 extension Company {    @objc(addEmployeeObject:)  @NSManaged public func addEmployee(_ value: Employee)    @objc(removeEmployeeObject:)  @NSManaged public func removeFromEmployee(_ value: Employee)    @objc(addEmployee:)  @NSManaged public func addEmployee(_ values: NSSet)    @objc(removeEmployee:)  @NSManaged public func removeFromEmployee(_ values: NSSet) } 

为了完整起见,了解此方法很有用,但是以某种方式它对我没有用,因此我删除了多余的代码,以免使项目混乱。

子句查询


在核心数据中,您不能像在SQL中那样在任何数据之间进行任意查询。 但是在相关对象之间,使用标准谓词很容易进行跟踪。 下面是一个查询示例,该查询选择了所有拥有指定名称雇员的公司:

 public static func getCompanyWithEmployee(name: String) -> [Company] { let request = NSFetchRequest<NSFetchRequestResult>(entityName: self.className()) request.predicate = NSPredicate(format: "ANY employee.firstName = %@", name) do { if let result = try moc.fetch(request) as? [Company] { return result } } catch { } return [Company]() } 

代码中的方法调用将如下所示:

 //  ,    let companies = Company.getCompanyWithEmployee(name: "") 

不要在查询中使用过渡字段;执行查询时未定义它们的值。 不会发生任何错误,但结果将不正确。

设置属性(字段)


您可能已经注意到Entity属性具有多个选项。
通过可选,名称中的所有内容均清晰可见。

swif中显示了使用标量类型的选项。 Objective-C不会将标量类型用于属性,因为它们不能为nil。 尝试通过KVC为属性分配标量值将导致运行时错误。 这清楚地说明了为什么Core Data中的属性类型与语言类型没有严格的对应关系。 在快速项目和混合项目中,可以使用标量类型属性。

运输属性是存储在数据库中的计算字段。 它们可以用于加密。 这些属性通过重写的访问器或根据需要分配基元来接收值(例如,重写的willSave和awakeFromFetch)。

属性访问器:


例如,如果您不需要使用计算所得的字段进行加密或其他操作,则无需考虑附件的属性。 同时,获取和分配属性值的操作不是“原子的”。 要了解我的意思,请参见下面的代码:

 //  let name = company.name //  company.willAccessValue(forKey: "name") let name = company.primitiveValue(forKey: "name") company.didAccessValue(forKey: "name") //  company.name = " " //  company.willChangeValue(forKey: "name") company.setPrimitiveValue(" ", forKey: "name") company.didChangeValue(forKey: "name") 

在NSManagedObject事件中使用原语,而不是通常的分配,以避免循环。 一个例子:

 override func willSave() {   let nameP = encrypt(field: primitiveValue(forKey: "name"), password: password)   setPrimitiveValue(nameP, forKey: "nameC")   super.willSave() }  override func awakeFromFetch() {   let nameP = decrypt(field: primitiveValue(forKey: "nameC"), password: password)   setPrimitiveValue(nameP, forKey: "name")   super.awakeFromFetch() } 

如果突然某个时候您必须将awakeFromFetch函数拧入项目中,那么您会惊讶于它的工作原理很奇怪,但是实际上在执行请求时根本没有调用它。 这是由于Core Data具有非常智能的缓存机制,并且如果选择已经在内存中(例如,因为您刚刚填充了此表),则不会调用该方法。 但是,我的实验表明,就计算值而言,可以安全地依靠awakeFromFetch的使用,如Apple的文档所述。 如果要进行测试和开发,则需要强制awakeFromFetch,请在请求之前添加managedObjectContext.refreshAllObjects()。

仅此而已。

感谢所有读完本书的人。

Source: https://habr.com/ru/post/zh-CN436510/


All Articles