这是苹果官方文档 Core Data Programming Guide 的渣翻译。
任何一个托管对象都链接到一个实体描述(一个NSEntityDescription实例),这个实体描述提供了这个对象的元数据以及一个用以跟踪对象图变更的托管对象上下文。对象会表示出它的属性的名字和关系,而这个对象的元数据包含了实体名字。
在一个托管对象上下文中,一个托管对象表示了一条在持久化存储中的记录。在一个上下文中,对于一条在持久化存储的记录,仅会存在一个相对应的托管对象,但是可能有一种情况是存在多个上下文,每一个上下文都包含了一个独立的表示这条记录的托管对象。换句话说,在数据记录和托管对象之间,不仅有一对一关系,还有一对多关系。
注意
Core Data不允许跨存储区建立实体关系。如果你需要创建某个存储区中一个对象
映射到另一个存储区的关系,可以考虑使用弱关系Weak Relationships(Fetched Properties)。
在托管对象模型中定义关系
当你开始创建一个关系的时候有很多事情需要决定。什么是目标实体(destination entity)?是一对一关系还是一对多关系?这个关系是否可选?如果是一对多关系,关系中的对象有没有最小或者最大的数目限制?当源对象被删除的时候怎么办?
在一个对象模型关系中,你有一个源实体(比如,Department)和一个目标实体(比如,Employee)。你可以在数据模型检视器(Data Model inspector)的关系面板中定义一个源实体和目标实体之间的关系。
图12-1 在数据模型检视器中的关系面板
关系的基本原则
一个关系指定了目标对象的实体或者父实体。这个实体可以是源实体本身(一个反身关系)。关系并不一定需要只引用一个实体类型。如果Employee实体有两个子实体,叫Manager和Assistant,并且Employee不是抽象的,那一个department的employees可以是由Employee(主实体),Manageer(Employee的子实体),Assistant(Employee的子实体),或者他们的混合使用组成的。
在关系面板的类型字段,你可以指定这个关系是一对一还是一对多的,基数是当前设置的实体。一对一关系由单独一个目标对象引用表示,而一对多则由一个可变Set集合表示。多对多关系则可以在两个实体之间设置一对多反向关系实现。怎么实现多对多关系的模型取决于你的语义模式设计。更多关于关系类型的细节,可以浏览多对多关系;
你还可以设置一个一对多关系中目标实体数量的上限或者下限。下限不能是0。你可以指定在一个department中的employee数量必须是在3到40之间的。你还可以指定这个关系是可选或者不可选的。如果一个关系是不可选的,那么一定要给这个关系的目标引用赋予一个可用的对象或者对象集合。
数量限制可选性在关系中是可以混合使用的。虽然你指定了上下限,但仍可以指定这个关系是可选的。这就表示可以在这个目标引用中没有任何对象,但如果有,必须要符合数量上下限的限制。
创建关系但是不创建对象
很重要地需要注意的是,简单地定义了一个关系,在创建了一个新的源对象之后并不会自动创建一个目标对象。就是说,定义一个关系就是类似于在一个标准的Objective-C类中声明了一个实例变量。参考一下例子:
OBJECTIVE-C
@interface AAAEmployeeMO : NSManagedObject
@property (nonatomic, strong) AAAAddressMO *address;
@end
SWIFT
class AAAEmployeeMO: NSManagedObject {
@NSManaged address: AAAAddressMO?
}
如果你创建了一个Employee的实例,就算你是通过代码实现的,也不能自动创建一个Address实例。类似的,如果你定义了一个Address实体,和从Employee映射的一个不可选的一对一关系,直接创建Employee实例并不会创建一个新的Address实例。同样的,在一个一对多关系中,如果你定义了一个不可选的下限数量是1、从Employee映射到Address,创建一个Employee实例之后也并不会创建一个新的Address实例。
反向关系
大部分的对象关系本身就是双向的。如果一个Department有一个映射到在这个Department中工作的Employee的一对多关系,那么Employee也会有一个从Employee映射到Department的一对一关系。主要的例外是fetched property,表示这是一个单向的、没有从目标指向源反向关系的关系。参考弱关系(Fetched Properties)。
强烈建议在有关系的两个实体两边都要进行关系建模,根据情况设置反向关系。Cora Data会在变更操作之后使用这些信息确保对象图的一致性(参考正确维护关系和对象图)。
关系删除规则
当将要删除一个源对象的时候,一个关系的删除规则指定了后续的处理程序。注意这里是将要删除,如果一个关系删除规则是Deny,那么可能这个源对象将不能被删除。再次回顾department的employees的关系,和一下不同的删除规则的效果。
Deny
如果至少有一个对象存在关系目标集合中(employees),那么不能删除源对象(department)。
例如,如果你想要删除department,你需要确保所有在这个department中的employees都被转移到了别的地方(或者销毁),否则,这个department不能被删除。
Nullify
删除两个对象之间的关系,但是不删除关联的对象。
这仅当department关系对于一个employee来说是可选的时候才有作用,或者你需要确保在下一次save操作前给每一个employees设置一个新的department。
Cascade
在你删除源对象的时候,会删除关系中的关联的目标对象。
例如,如果你删除一个department,同时会删除所有在这个department中的employees。
No Action
不对关系的目标对象做任何操作。
例如,你删除了一个department,但是保留了所有employees,及时他们认为他们还属于之前的department。
需要明白的是前三条规则在不同的情境中都是十分有用的。对于任何关系,需要你来基于业务逻辑选择哪种是最适合的。No Action一般来说没什么用,因为如果你用了这个规则,就有可能让对象图处于不一致的状态中(employees有一个映射到被删除了的department的关系)。
如果你用了No Action规则,你需要确保关系图的一致性。你需要为每一个反向关系设置有效的值。如果是你需要去维护一个一对多关系,并且有大量的目标对象,这种情况下可能是有成效的。
<span id="manipulating_relationship_object_gragh"></span>
操作关系和对象图完整性
当你修改一个对象图,维护引用的完整性是很重要的。Core Data让你修改托管对象之间的关系更加容易,而不会引起引用完整性错误。大部分这些操作源自于托管对象模型中的关系描述。
当你需要变更一个关系,Core Data会自动为你维护对象图的一致性,所以你只需要变更任意一边的关系。这个特性对一对一、一对多、多对多关系都是适用的。参考以下例子。
图12-2 反向关系的完整性维护
如果没有Core Data框架,你必须要写好几行的代码去维护对象图的一致性。此外你还需要知道Department类的实现,知道是否存在employee映射到department的反向关系需要设置。这可能随着应用的发展会发生变更。用了Core Data框架,仅适用一行代码就能将这一切变得简单:
OBJECTIVE-C
anEmployee.department = newDepartment;
SWIFT
anEmployee.department = newDepartment
或者,你还可以使用:
OBJECTIVE-C
[newDepartment addEmployeeObject:anEmployee];
SWIFT
newDepartment.mutableSetValueForKey("employees").addObject(employee)
上述两种方法都有同一个效果:参照于应用的托管对象模型,框架会自动从当前对象图的状态决定那些关系需要创建,哪些需要销毁。
<span id="many_many_relationship"></span>
多对多关系
你使用两个一对多关系定义了一个多对多关系。第一个一对多关系从第一个实体(源实体)映射到第二个实体(目标实体)。第二个一对多关系从第二个实体(原来的目标实体)映射到第一个实体(原来的源实体)。然后你设置他们互为反向关系。(如果你有一些数据库关系的背景知识,可能会有一些顾虑,不要害怕:如果你使用SQLite数据库,Core Data会自动为你创建联接表join table)。
重要事项
你必须在双边都定义一对多关系,即是,你必须指定两个关系,互为反向关系。
你不能只定义一边的一对多关系并当做多对多关系来使用。如果这样做,会产生引用完整性问题。
这里的关系配置为一个实体中某个关系反向指向了自己(通常称为反身关系)。例如,如果一个employee允许有超过一个manager(并且一个manager可以拥有超过一个直属下级汇报人directReport),然后你就可以定义一个一对多关系directReports,是从一个Employee实体反向指向自己的一对多关系。managers,同样也是反向指向Employee实体的一个关系。在图12-3中阐明了这个情况。
图12-3 反身多对多关系例子
根据语义进行关系建模
思考关系的语义和如何进行建模。一个比较通俗的例子是开始要对一个“friends”进行多对多关系建模。虽然你必定是你表哥的表弟,不管你表哥是否喜欢,但是你不一定是你朋友的朋友。对于这样的关系,可以使用一个媒介(join)实体。这样做的好处是你还可以给这个关系增添更多的信息。例如,一个FriendInfo实体可能包含了使用一个ranking属性来表示的友谊亲密度指标,如图12-4所示。
图12-4 一个使用了FriendInfo实体作为媒介实体表示的友谊关系模型
在这个例子中,Person有两个指向FriendInfo的一对多关系:friends表示源person的朋友,befriendedBy则表示那些把源friend当做朋友的人。FriendInfo表示某一方在这个友谊关系的相关信息。任意一个实例可以展示出谁是源,和哪些人认为这个人是他们的朋友。如果双方都有同样的想法,那么就会有对应的互相设置为源source和朋友friend的实例。处理这类模型的时候这里有几个需要考虑的地方:
- 创建一个从一个人指向另一个人的友谊关系friendship,必须要先创建一个FriendInfo实例。如果双方都喜欢对方,你需要创建两个FriendInfo实例。
- 要销毁一个友谊关系,你需要删除相应的FriendInfo实例。
- 从Person指向FriendInfo关系的删除规则应该是Cascade(级联删除)。即是说,如果一个person从存储区删除了,那么这个FriendInfo实例就无效了,所以必须要删除掉。
所以,从FriendInfo指向Person的关系必定是可选的——当源source或者friend变成了null,一个FriendInfo实例将会无效。 - 要找出所以某个person的朋友,你必须要查询所有friends关系中的friend,例如:
OBJECTIVE-C
NSSet *personsFriends = [aPerson valueForKeyPath:@"friends.friend"];
SWTIFT
let personsFriends = aPerson.valueForKeyPath("friends.friend")
- 要找出所有把某个person作为朋友的人,你需要查询所有befreiendedBy关系中的source,例如:
OBJECTIVE-C
NSSet *befriendedByPerson = [aPerson valueForKeyPath:@"befriendedBy.source"];
SWIFT
let befriendedByPerson = aPerson.valueForKeyPath("befriendedBy.source")
不支持跨存储区关系
小心不要创建从一个持久化存储实例指向另一个持久化存储的关系,因为Core Data并不支持。如果你需要创建在不同存储中两个实体之间的关系,一般你可以使用fetched properties。参考以下章节。
<span id="weak_relationship"></span>
弱关系(Fetched Properties)
Fetched properties表示弱指向、单向的关系。在employees和department的例子中,例如某个department的fetched property代表最近雇佣的人(Recent Hires)。Employee并没一个对Recent Hires的反向关系。通常来说,fetched properties最适合用来对跨存储关系、低耦合双向关系、临时相似类型簇群进行建模。
一个fetched property跟一个关系很像,但是在以下几个重要的点是不一样的:
- 相比一个指向关系,一个fetched property的值是使用一个fetch request来计算获得的。(这个fetched reqeust一般使用一个predicate来限定约束返回的结果)
- 一个fetched property有一个数组(NSArray)并非集合(NSSet)来表示。关联的fetch request可以绑定设置排序属性,自然返回的fetched property就会被排序了。
- 一个fetched property是懒计算的,使用后会被缓存起来。
图12-5 为一个实体增加一个fetched property
在某个方面上说你可以认为一个fetched property是类似于一个智能播放列表,但不是动态的。如果在目标实体的对象变更了,你必须要重新请求计算fetched property来同步数据。你可以使用frefreshObject:mergeChanges:来手动刷新properties——当对象将要被置为fault的时候会触发这个property关联的fetch request再次执行。
你可以在predicate中使用两个fetched property的特殊变量,$FETCH_SOURCE和$FETCHED_PROPERTY。source指向这个propert所属的托管对象,你可以创建key paths来引用source中的属性,例如university.name LIKE [c] $FETCH_SOURCE.searchTerm。$FETCHED_PROPERTY是这个实体fetched property的描述。这个property描述拥有一个userInfo字典,你可以用来构造任何你需要的键值对。因此你可以修改任何fetched property中predicate或者关联的对象的表达式。
为了弄明白这个变量的原理,这里有一个带有一个目标实体Author的fetched property和一个predicate格式,(university.name LIKE [c] $FETCH_SOURCE.searchTerm) AND (favoriteColor LIKE [c] $FETCHED_PROPERTY.userInfo.color)。如果源对象有一个属性searchTerm是“Cambridge”,而fetched property有一个user info字典,包含了一个key——“color”,一个value——“Green”,然后返回结果的predicate为(university.name LIKE [c] "Cambridge") AND (favoriteColor LIKE [c] "Green")。这个fetched property会匹配出任何在“Cambridge”且最喜爱的颜色为“Green”的Authors。如果你变更了源对象中的“searchTerm”属性为“Durham”,那么predicate将会是(university.name LIKE [c] "Durham") AND (favoriteColor LIKE [c] "Green")。
使用fetched properties最大的约束就是你不能使用替代物去变更predicate的结构 —— 例如,你不能修改LIKE predicate成一个混合predicate,也不能变更操作符(在这个例子中,就是LIKE [c])。