在上一篇文章中 ,我们了解了NSManagedObject
以及使用Core Data创建,读取,更新和删除记录的难易程度。 但是,在该讨论中我没有提及关系。 除了需要注意的一些警告以外,关系和属性一样容易操作。 在本文中,我们将专注于关系,并且还将继续对NSFetchRequest
探索。
先决条件
我在本系列的“核心数据”中介绍的内容适用于iOS 7+和OS X 10.10+,但是重点将放在iOS上。 在本系列中,我将使用Xcode 7.1和Swift 2.1。 如果您更喜欢Objective-C,那么我建议阅读我先前关于Core Data framework的系列文章 。
1.关系
我们已经在Core Data模型编辑器中处理过关系,因此我将要告诉您的内容听起来很熟悉。 关系就像属性一样,使用键值编码进行访问。 请记住,我们在本系列前面创建的数据模型定义了一个Person实体和一个Address实体。 一个人链接到一个或多个地址,而一个地址链接到一个或多个人。 这是多对多的关系。
要获取人员的地址,我们只需在人员( NSManagedObject
的实例)上调用valueForKey(_:)
,然后将地址作为密钥传递即可。 请注意, 地址是我们在数据模型中定义的关键。 您期望什么类型的对象? 大多数刚接触Core Data的人都希望使用排序数组,但是Core Data返回的是未排序的集合。 使用集有其优势,您将在后面学习。
创建记录
理论足够,请从上一篇文章中打开该项目,或者从GitHub克隆该项目。 让我们先创建一个人,然后将其链接到地址。 要创建一个人,请打开AppDelegate.swift并更新application(_:didFinishLaunchingWithOptions:)
,如下所示。
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Create Person
let entityPerson = NSEntityDescription.entityForName("Person", inManagedObjectContext: self.managedObjectContext)
let newPerson = NSManagedObject(entity: entityPerson!, insertIntoManagedObjectContext: self.managedObjectContext)
// Populate Person
newPerson.setValue("Bart", forKey: "first")
newPerson.setValue("Jacobs", forKey: "last")
newPerson.setValue(44, forKey: "age")
return true
}
如果您已阅读上一篇文章,这应该看起来很熟悉。 如下所示,创建地址看起来很相似。
// Create Address
let entityAddress = NSEntityDescription.entityForName("Address", inManagedObjectContext: self.managedObjectContext)
let newAddress = NSManagedObject(entity: entityAddress!, insertIntoManagedObjectContext: self.managedObjectContext)
// Populate Address
newAddress.setValue("Main Street", forKey: "street")
newAddress.setValue("Boston", forKey: "city")
因为Address实体的每个属性都标记为optional ,所以我们不需要为每个属性分配值。 在示例中,我们仅设置记录的街道和城市属性。
建立关系
要将newAddress
链接到newPerson
,我们调用valueForKey(_:)
,并传入addresses
作为键。 我们传入的值是一个包含newAddress
的NSSet
实例。 请看下面的代码块以进行澄清。
// Add Address to Person
newPerson.setValue(NSSet(object: newAddress), forKey: "addresses")
do {
try newPerson.managedObjectContext?.save()
} catch {
let saveError = error as NSError
print(saveError)
}
我们在newPerson
的托管对象上下文上调用save()
以将更改传播到持久性存储。 请记住,在托管对象上下文上调用save()
托管对象上下文的状态。 这意味着newAddress
以及我们刚刚定义的关系也将写入后备存储。
您可能想知道为什么我们没有将newPerson
链接到newAddress
,因为我们确实在数据模型中定义了逆关系。 核心数据为我们创造了这种关系。 如果关系具有逆向关系,则Core Data会自动处理此关系。 您可以通过向newAddress
询问其persons
来验证这一点。
获取和更新关系
更新关系也不难。 唯一需要注意的是,我们需要从不NSSet
实例Core Data手中添加或删除元素。 但是,为了NSKeyValueCoding
此任务, NSKeyValueCoding
协议声明了一种便捷方法mutableSetValueForKey(_:)
,该方法返回NSMutableSet
对象。 然后,我们可以简单地从集合中添加或删除项目以更新关系。
看一下下面的代码块,在其中我们创建另一个地址并将其与newPerson
关联。 我们通过在newPerson
上调用mutableSetValueForKey(_:)
newPerson
可变集添加otherAddress
来实现。 无需告诉Core Data我们已经更新了关系。 Core Data跟踪它给我们的可变集并更新关系。
// Create Address
let otherAddress = NSManagedObject(entity: entityAddress!, insertIntoManagedObjectContext: self.managedObjectContext)
// Set First and Last Name
otherAddress.setValue("5th Avenue", forKey:"street")
otherAddress.setValue("New York", forKey:"city")
// Add Address to Person
let addresses = newPerson.mutableSetValueForKey("addresses")
addresses.addObject(otherAddress)
删除关系
您可以通过调用setValue(_:forKey:)
删除关系,传入nil
作为值,并将关系的名称作为键。 在下一个代码段中,我们取消每个地址与newPerson
链接。
// Delete Relationship
newPerson.setValue(nil, forKey:"addresses")
2.一对一和一对多关系
一对一关系
即使我们的数据模型没有定义一对一的关系,您也已经学到了处理这种关系所需的一切。 一对一关系与使用属性相同。 唯一的区别是,您从valueForKey(_:)
获取的值和传递给setValue(_:forKey:)
是一个NSManagedObject
实例。
让我们更新数据模型来说明这一点。 打开Core_Data.xcdatamodeld并选择Person实体。 创建一个新的关系并将其命名为配偶 。 将“ 人”实体设置为目的地,并将配偶关系设置为反关系。
![给人添加一对一关系](https://cms-assets.tutsplus.com/uploads/users/41/posts/25070/image/figure-add-one-to-one-relationship.jpg)
如您所见,可以创建一个关系,其中关系的目的地是与定义该关系的实体相同的实体。 还要注意,我们总是设置关系的倒数。 正如Apple的文档所述,在极少数情况下,您想要创建没有反向关系的关系。
您知道如果要构建和运行该应用程序会发生什么? 没错,应用程序将崩溃。 由于我们更改了数据模型,因此现有的后备存储(在此示例中为SQLite数据库)不再与数据模型兼容。 要解决此问题,请从设备或模拟器中删除该应用程序,然后运行该应用程序。 不过请放心,我们将在以后的部分中使用迁移更加优雅地解决此问题。
如果您可以运行该应用程序而没有问题,那么该是下一步了。 回到应用程序委托类,并添加以下代码块。
// Create Another Person
let anotherPerson = NSManagedObject(entity: entityPerson!, insertIntoManagedObjectContext: self.managedObjectContext)
// Set First and Last Name
anotherPerson.setValue("Jane", forKey: "first")
anotherPerson.setValue("Doe", forKey: "last")
anotherPerson.setValue(42, forKey: "age")
要设置anotherPerson
为配偶newPerson
,我们调用setValue(_:forKey:)
上newPerson
,并通过在anotherPerson
和"spouse"
作为参数。 我们可以通过调用相同的结果setValue(_:forKey:)
上anotherPerson
,传递newPerson
和"spouse"
作为参数。
// Create Relationship
newPerson.setValue(anotherPerson, forKey: "spouse")
一对多关系
让我们看一下一对多关系。 打开Core_Data.xcdatamodeld ,选择Person实体,然后创建一个名为children的关系。 将目的地设置为Person ,将类型设置为To Many ,并将逆关系暂时保留为空。
创建另一个名为父亲的关系,将目的地设置为Person ,然后将反向关系设置为子级 。 这将自动填充我们刚才留下空白的子级关系的逆关系。 现在,我们已经建立了一对多的关系,即一个父亲可以生很多孩子,但是一个孩子只能有一个亲生父亲。
回到应用程序委托并添加以下代码块。 我们通过向核心数据请求关键子项的可变集并将新记录添加到可变集中,来创建另一个Person记录,设置其属性,并将其设置为newPerson
的子newPerson
。
// Create a Child Person
let newChildPerson = NSManagedObject(entity: entityPerson!, insertIntoManagedObjectContext: self.managedObjectContext)
// Set First and Last Name
newChildPerson.setValue("Jim", forKey: "first")
newChildPerson.setValue("Doe", forKey: "last")
newChildPerson.setValue(21, forKey: "age")
// Create Relationship
let children = newPerson.mutableSetValueForKey("children")
children.addObject(newChildPerson)
以下代码块通过设置anotherChildPerson
的father
属性来实现相同的结果。 结果是newPerson
成为anotherChildPerson
的父亲,而anotherChildPerson
成为newPerson
的孩子。
// Create Another Child Person
let anotherChildPerson = NSManagedObject(entity: entityPerson!, insertIntoManagedObjectContext: self.managedObjectContext)
// Set First and Last Name
anotherChildPerson.setValue("Lucy", forKey: "first")
anotherChildPerson.setValue("Doe", forKey: "last")
anotherChildPerson.setValue(19, forKey: "age")
// Create Relationship
anotherChildPerson.setValue(newPerson, forKey: "father")
3.更多提取
就复杂性而言,我们的示例应用程序的数据模型已经增长了很多。 我们已经创建了一对一,一对多和多对多的关系。 我们已经看到创建记录(包括关系)是多么容易。 如果我们还希望能够从持久性存储中提取数据,那么我们需要了解有关获取的更多信息。 让我们从一个简单的示例开始,在该示例中,我们将看到如何对获取请求返回的结果进行排序。
排序描述符
为了对从托管对象上下文中返回的记录进行排序,我们使用NSSortDescriptor
类。 看一下下面的代码片段。
// Create Fetch Request
let fetchRequest = NSFetchRequest(entityName: "Person")
// Add Sort Descriptor
let sortDescriptor = NSSortDescriptor(key: "first", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
// Execute Fetch Request
do {
let result = try self.managedObjectContext.executeFetchRequest(fetchRequest)
for managedObject in result {
if let first = managedObject.valueForKey("first"), last = managedObject.valueForKey("last") {
print("\(first) \(last)")
}
}
} catch {
let fetchError = error as NSError
print(fetchError)
}
我们通过传入我们感兴趣的实体Person来初始化获取请求。 然后,我们创建一个NSSortDescriptor
通过调用对象init(key:ascending:)
,传递我们想通过排序, 首先是实体的属性,以及一个布尔值,指示记录是否需要递增或递减顺序进行排序。
通过设置获取请求的sortDescriptors
属性,将排序描述符绑定到获取请求。 因为sortDescriptors
属性的类型为[NSSortDescriptor]?
,可以指定多个排序描述符。 稍后我们将介绍这个选项。
代码块的其余部分应该看起来很熟悉。 提取请求被传递到托管对象上下文,该托管对象上下文在我们调用executeFetchRequest(_:)
时执行提取请求。 请记住,后者是一个throwing方法,这意味着我们使用try
关键字并在do-catch
语句中执行fetch请求。
运行该应用程序,并在Xcode的控制台中检查输出。 输出应类似于以下所示。 如您所见,记录按名字进行排序。
Bart Jacobs
Jane Doe
Jim Doe
Lucy Doe
如果您在输出中发现重复项,请确保注释掉我们之前编写的用于创建记录的代码。 每次运行该应用程序时,都会创建相同的记录,从而导致记录重复。
就像我提到的,可以组合多个排序描述符。 让我们按记录的姓氏和年龄进行排序。 我们首先将第一个描述符的关键字设置为last 。 然后,我们创建另一个带有年龄键的排序描述符,并将其添加到排序描述符数组中。
// Add Sort Descriptor
let sortDescriptor1 = NSSortDescriptor(key: "last", ascending: true)
let sortDescriptor2 = NSSortDescriptor(key: "age", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor1, sortDescriptor2]
输出显示数组中排序描述符的顺序很重要。 记录首先按姓氏排序,然后按年龄排序。
Lucy Doe (19)
Jim Doe (21)
Jane Doe (42)
Bart Jacobs (44)
谓词
排序描述符非常好并且易于使用,但谓词才是真正使获取在Core Data中变得强大的要素。 排序描述符告诉Core Data如何对记录进行排序。 谓词告诉Core Data您感兴趣的记录。我们将使用的类是NSPredicate
。
让我们从获取Doe家族的每个成员开始。 这很容易做到,语法会使您想起一些SQL。
// Fetching
let fetchRequest = NSFetchRequest(entityName: "Person")
// Create Predicate
let predicate = NSPredicate(format: "%K == %@", "last", "Doe")
fetchRequest.predicate = predicate
// Add Sort Descriptor
let sortDescriptor1 = NSSortDescriptor(key: "last", ascending: true)
let sortDescriptor2 = NSSortDescriptor(key: "age", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor1, sortDescriptor2]
// Execute Fetch Request
do {
let result = try self.managedObjectContext.executeFetchRequest(fetchRequest)
for managedObject in result {
if let first = managedObject.valueForKey("first"), last = managedObject.valueForKey("last"), age = managedObject.valueForKey("age") {
print("\(first) \(last) (\(age))")
}
}
} catch {
let fetchError = error as NSError
print(fetchError)
}
除了通过调用init(format:arguments:)
创建NSPredicate
对象并通过设置后者的predicate
属性将谓词与获取请求绑定在一起之外,我们没有做太多改变。 请注意, init(format:arguments:)
方法接受可变数量的参数。
谓词格式字符串将%K
用作属性名称,将%@
用作值。 如《 Predicate编程指南》中所述 , %K
是键路径的变量参数替代,而%@
是对象值的变量参数替代。 这意味着我们示例的谓词格式字符串的计算结果为last == "Doe"
。
如果运行该应用程序并在Xcode的控制台中检查输出,则应看到以下结果:
Lucy Doe (19)
Jim Doe (21)
Jane Doe (42)
我们可以使用许多运算符进行比较。 除了=
和==
,就核心数据而言,它们是相同的,还有>=
和=>
, <=
和=>
, !=
和<>
以及>
和<
。 我鼓励您尝试使用这些运算符,以了解它们如何影响获取请求的结果。
以下谓词说明了如何使用>=
运算符仅获取age属性大于30的 Person记录。
let predicate = NSPredicate(format: "%K >= %i", "age", 30)
我们还有用于字符串比较的运算符, CONTAINS
, LIKE
, MATCHES
, BEGINSWITH
和ENDSWITH
。 让我们取每个人的记录,其名称CONTAINS
信j
。
let predicate = NSPredicate(format: "%K CONTAINS %@", "first", "j")
如果运行应用程序,则结果数组将为空,因为默认情况下字符串比较区分大小写。 我们可以通过添加如下修饰符来更改它:
let predicate = NSPredicate(format: "%K CONTAINS[c] %@", "first", "j")
您还可以使用关键字AND
, OR
和NOT
来创建复合谓词。 在以下示例中,我们获取每个名字都包含字母j
并且小于30
。
let predicate = NSPredicate(format: "%K CONTAINS[c] %@ AND %K < %i", "first", "j", "age", 30)
谓词还使基于它们的关系来获取记录变得非常容易。 在以下示例中,我们获取父亲的名字等于Bart
每个人。
let predicate = NSPredicate(format: "%K == %@", "father.first", "Bart")
上述谓词按预期工作,因为%K
是一个关键路径的变量参数替换,而不仅仅是一个关键 。
您需要记住的是,谓词使您无需了解存储就可以查询后备存储。 即使谓词格式字符串的语法在某些方面让人联想到SQL,但后备存储是SQLite数据库还是内存存储都没关系。 这是一个非常强大的概念,并非Core Data独有。 Rails的Active Record是这种范例的另一个很好的例子。
谓词比我在本文中向您介绍的要多得多。 如果您想了解更多有关谓词的知识,建议您在Apple的《 谓词编程指南》中学习一下。 在本系列的后续几篇文章中,我们还将与谓词一起使用。
结论
您现在已经掌握了Core Data的基础知识,是时候通过创建利用Core Data功能的应用程序开始使用框架了。 在下一篇文章中,我们遇到了核心数据框架的另一个重要类NSFetchedResultsController
。 此类将帮助我们管理记录的集合,但是您将了解到它的作用远不止于此。
翻译自: https://code.tutsplus.com/tutorials/core-data-and-swift-relationships-and-more-fetching--cms-25070