本文部分内容来自 Objc.io 的《Core Data》一书,买来一个月后觉得39美元总体还是花得值得的,推荐购买。
Fetch requests 并非获取 managed objects 的唯一途径,而且,应该尽可能避免 fetch。因为 fetch 操作会遍历整个 Core Data Stack,代价很大,是重大的性能瓶颈。获取 managed objects 的另外一个重要途径是 relationship。
Creating Relationships
一般关系
Relationship 有三种:一对一(to-one),一对多(to-many),多对多(many-many)。多对多关系的对应关系也应该是多对多关系。建立关系时尽量避免单向关系,这样不利于 Core Data 维护对象之间的关系。在 Model Editor 里设置关系时注意设置逆向关系 Inverse Relationship,这样 Core Data 可以替我们完成很多工作。
部门 Department 和职员 Employee 的关系设置:
这里部门与职员的关系是一对多,职员与部门的关系是一对一。
一对一关系和 managed object 的其他属性没有多大区别,Xcode 生成的子类里该关系的属性类型就是关系目标的类型;一对多关系里,为了保持维护的对象唯一,子类使用 Set 来维护对对象的引用。若勾选了 Ordered,则使用 NSOrderdSet 类。使用有序关系并没有真的对里面的对象进行排序,只是方便了索引。
extension Department {
@NSManaged var name: String?
@NSManaged var employees: NSOrderedSet?
}
extension Employee {
@NSManaged var name: String?
@NSManaged var department: Department?
}
不一般关系⊙▂⊙
上面的例子里关系目标都是其他实体 Entity,关系也可以指向自身类型的 Entity,比如,利用 Person 建立一个族谱,那么关系的目标对象都是 Person。这里除了关系引用的是自身类型,也没有什么特别的了。
还有一种比较特别:单向关系。上面也提到了,尽量不要建立单向关系。因为在单向关系里,一方被删除了的话,另一方无法得知。使用单向关系时必须小心。
PS: Core Data 不支持跨 Store 的关系。
Accessing and Manipulating Relationships
访问关系
访问关系有两种途径,一种是和访问普通的对象属性一样:
let employees: NSOrderedSet = aDepartment.employees
let department: Department = anEmployee.department
另外一种是使用 KVC 方法,如果传递的 key 不是 Modal 里定义的属性,将会抛出异常:
let employees = aDepartment.valueForKey("employees") as? NSOrderedSet
let department = anEmployee.valueForKey("department") as? Department
除此之外,关系还支持 keypath 访问,path 支持普通的属性 perporty 和关系 relationship:
let departmentName = anEmployee.valueForKeyPath("department.name") as? String
修改关系
修改关系这件事需要好好说明一下:我们只需要修改关系一方,Core Data 会自动替我们处理好剩下的事情。比如下面的情况:
只需要:
//方法1:
anEmployee.department = newDepartment
//方法2:
anEmployee.setValue(newDepartment, forKey:"department")
或者:
//如果没有勾选 Ordered 选项,使用 mutableSetValueForKey(_:)
newDepartment.mutableSetValueForKey("employees").addObject(employee)
//如果勾选了,使用 mutableOrderedSetValueForKey(_:)
newDepartment.mutableOrderedSetValueForKey("employees").addObject(employee)
只需要使用上面的一种方法就可以了。
如果像批量更改部门的职员构成怎么办,单个移除以及添加很麻烦,使用 KVC 方法。
newDepartment.setValue(newEmployees, forKey:"employees")
在 Department 这一端,因为直接访问employees
得到的一个无法更改的量,只能使用mutableSetValueForKey(_:)
或mutableOrderedSetValueForKey(_:)
来进行个体的修改,或者使用setValue(_, forKey:)
来进行整体的修改。处于性能的原因,set<Key>:这类方法比如setEmployees:
不能用来修改关系。
NSManagedObject
重写了valueForKey:
和setValue:forKey:
以及mutableSetValueForKey(_:)
这三个 KVC 方法,当 key 不是在 modal 里定义的属性时,这三个方法都会抛出异常。你不应该在子类中重写这三个方法。
Delete Rule
上面只需要在修改一端修改关系剩下的事情由 Core Data 替我们处理了得益于 Delete Rule 的设计。删除规则决定了删除对象时它的关系怎么处理的行为。Core Data 提供了四种删除规则,下面还是用部门与员工之间的关系来举例:
- 拒绝 Deny
如果关系目标里还有对象,比如要删除(撤销)某个部门,但该部门还有一个员工,那么是无法删除(撤销)该部门的,只有该部门里所有的员工被调往其他部门或是被删除(解雇)了才能删除(撤销)该部门。 - 失效 Nullify
移除对象之间的关系但是不删除对象。只有当一方关系是可有可无的时候才有意义,比如员工有没有部门都无所谓,那么删除该对象时只会将其关系设置为空而不会删除对象本身。 - 连坐 Cascade
在这种规则下,可以把一个部门一锅端。删除(撤销)某部门,部门里的员工也会被全部解雇(删除)。 - 不作为 No Action
什么也不做。部门被删除(撤销)后,里面的员工还不知道,以为自己还在这个部门工作呢。
前三种删除规则还是比较清晰的,都有合适的使用场景,而最后一种比较危险,需要你自己来确定关系里的对象是否还存在。
Relationship Faults
访问 managed object 的 relationship property 时,relationship 对应的 managed object 如果在 context 中不存在,那么被 fetch 进内存时会处于 faults 状态,即使已经存在也不会主动填充 managed object 中的数据,无论原来的 managed object 处于 faults 状态还是已经填充了全部数据。而且,无论是 to-one relationship 还是 to-mant relationship都是这样。这个特性对于维持较低的内存占用具有重要意义。
Relationship faults 有两层:访问 relationship 时,这时候 Core Data 做的仅仅是根据 relationship 的 objectID 来获取相应的 managed object,并不会填充数据;访问 relationship 上的某个属性时,relationship 才会填充该属性对应的数据。
Reference Cycles
关系一般都是双向的,而且关系并不想其他对象一样有强引用和弱引用的区别,在这种情况下,当关系的双方都在内存中后,自然而然就形成了引用循环。打破引用循环的唯一方法是刷新关系中的一方,使用 context 的refreshObject(_:mergeChanges:)
来刷新对象。
context.refreshObject(managedObejct, mergeChanges:false)
参数mergeChanges
为 false 时,对象会重新进入 faults 状态,这会移除对象里所有的数据,包括与其他对象之间的关系。需要注意的是,在这个参数配置下,该方法会放弃对象身上所有没有保存的变化。
至于何时打破引用循环这取决于应用自身的需要。比如,将当前的 ViewController 从 stack 中移除,你不再需要这些数据了,可以将对象转变为 faults状态;或者应用进入后台,你也可以这样做降低内存占用避免被系统杀掉。