CoreData基本入坑指南
写在前面
开发中使用Core Data的确是要配置挺多内容的,首先要配置Model-Context-Coordinator-Store、配置Store的数据迁移策略(以及可能的数据迁移),还要处理多线程读写的问题。想用CoreData但又不想写太多这样业务无关代码时,MagicalRecord算是一个不错的选择。但像我这样用上了Core Data但没用(或者不想用)MagicalRecord之类的工具库时,做数据版本迁移是迟早的事情嘛。
使用Core Data,基本问题涉及数据模型定义、Model-Context-Coordinator-Store配置、CURD操作,还有现在要说的数据版本/迁移,以后要说的多线程问题。最近刚好处理版本迁移问题,简单记录下,基本配置以及多线程等问题迟些再补充。
关于Core Data以及它的数据版本迁移,可以先看下以下资料:
数据模型版本/映射模型
数据模型
Xcode可以直接在模型文件上创建不同版本的模型定义。
- 定义新版本:选中.xcdatamodeld模型文件(其实是文件夹),菜单Editor->Add Model Version,可选择基于当前特定版本的模型创建新版本的模型定义;创建后,.xcdatamodeld文件夹下增加.xcdatamodel的模型文件(其实也是文件夹);
- 指定使用版本:选中.xcdatamodeld模型文件,在Xcode右侧工具栏中,Model Version可以选择模型所使用的版本,选中的版本带有绿色勾选高亮;
sublime打开.xcdatamodeld目录可以一窥CoreData的模型文件(都提供给IDE使用):
首先是增加了一个.xccurrentversion的plist版本记录文件,内容很简单,仅是用一个key来表明当前所选的model文件:1
2
3
4
5
6
7
8
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>XXXModel_v2.xcdatamodel</string>
</dict>
</plist>
.xcdatamodel/contents则是模型定义的xml文件(仅作示例):1
2
3
4
5
6
7
8
9
10
11
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="10171" systemVersion="15D21" minimumToolsVersion="Xcode 7.0">
<entity name="FDMessage" representedClassName="FDMessage" syncable="YES">
<attribute name="clientID" optional="YES" attributeType="Integer 32" defaultValueString="0" syncable="YES"/>
<attribute name="content" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="draft" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="XXXXPostDraft" inverseName="game" inverseEntity="IMPPostDraft" syncable="YES"/>
</entity>
<elements>
<element name="FDMessage" positionX="-63" positionY="-18" width="128" height="180"/>
</elements>
</model>
内容除了记录编辑器的一些信息,主要还是模型信息,且基本与Xcode上可视化的配置是对应的。
elements
包含模型文件中的实体(Entity)列表,positionX
、positionY
、width
、height
暂未去探讨实际意义(可能是跟序列化过程中的数据布局相关);entity
则是每个实体的配置信息,包括普通属性attribute
、关系(属性)relationship
(当然还有fetched property此处未有)。name
是实体名称,representedClassName
则是类名(syncable是iCloud相关?);可以看到Core Data实体名称跟类名是可以独立的,这也是我们项目重构时可以利用的特性(已有开源库)。attribute
属性,包含属性名name
、是否可选optional
、属性类型attributeType
、默认值defaultValueString
、syncable未知特性relationship
关系,包含关系名name
、是否可选optional
、关联数maxCount
(一对多|一对一)、删除规则deleteRule
、目标实体destinationEntity
、本实体在目标实体中的关系名inverseName
、inverseEntity
,syncable
这些模型定义配置文件由Xcode编译后,会在.app目录下生成CoreData运行时使用的模型定义文件(夹)XXXModel.momd。
对于单一版本的模型文件,.momd中包含二进制格式XXXModel.mom模型定义以及一个biplist格式的版本信息文件VersionInfo.plist。对于多版本的模型文件.momd中则对应每个版本的模型定义生成一个.mom,以及.omo文件。你可能会好奇mom文件是什么呀?其实在Xcode5之后,通过 .xcdatamodel > Editor > Import可以完全复原数据模型的定义。omo又是什么呢,其实是当前版本模型定义数据的另一种格式,stackoverflow上已经有回答了:
At WWDC this year I talked to the Core Data engineers in the labs and they told me that the
.omo
file is just an optimized version of the.mom
file. The.mom
file is a binary plist while the.omo
is some other sort of format that’s faster to load.They told me that you could safely remove the
.omo
file and Core Data would load from the—slightly slower—.mom
file instead. They told me that doing so would only result in an additional few milliseconds of load time (which begs the question of why they bothered to optimize it in the first place). —stackoverflow by Ben Dolman, Jun 9’ 14
转换一下格式plutil -convert xml1 VersionInfo.plist -o Version.plist
可以看到VersionInfo.plist记录的是模型各个版本文件中实体配置的哈希:
1 |
|
可以看到在v2版本中实体XXXXPostBar改变了。CoreData将对发生改变的实体、依赖版本间映射关系进行数据迁移。(p.s .xcdatamodel文件Xcode不支持直接delete,可以尝试对.xcdatamodeld先remove reference,手动删除.xcdatamodel,再重新将.xcdatamodel添加到project)
映射模型
不同版本的数据迁移,需要知道模型版本间的数据关系,Xcode使用.xcmappingmodel来编辑记录这种映射关系。.xcmappingmodel同样是一文件夹,包含xcmapping.xml。.xcmapping.xml内容示例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<database>
<databaseInfo>
<version>134481920</version>
<UUID>701B3B37-7743-4E35-8C9C-CA081667974B</UUID>
<nextObjectID>114</nextObjectID>
<metadata>
<plist version="1.0">
<dict>
<key>NSPersistenceFrameworkVersion</key>
<integer>641</integer>
<key>NSStoreModelVersionHashes</key>
<dict>
<key>XDDevAttributeMapping</key>
<data>0plcXXRN7XHKl5CcF+fwriFmUpON3ZtcI/AfK748aWc=</data>
<key>XDDevEntityMapping</key>
<data>qeN1Ym3TkWN1G6dU9RfX6Kd2ccEvcDVWHpd3LpLgboI=</data>
<key>XDDevMappingModel</key>
<data>EqtMzvRnVZWkXwBHu4VeVGy8UyoOe+bi67KC79kphlQ=</data>
<key>XDDevPropertyMapping</key>
<data>XN33V44TTGY4JETlMoOB5yyTKxB+u4slvDIinv0rtGA=</data>
<key>XDDevRelationshipMapping</key>
<data>akYY9LhehVA/mCb4ATLWuI9XGLcjpm14wWL1oEBtIcs=</data>
</dict>
<key>NSStoreModelVersionHashesVersion</key>
<integer>3</integer>
<key>NSStoreModelVersionIdentifiers</key>
<array>
<string></string>
</array>
</dict>
</plist>
</metadata>
</databaseInfo>
<object type="XDDEVENTITYMAPPING" id="z102">
<attribute name="sourcename" type="string">IMPRecent</attribute>
<attribute name="mappingtypename" type="string">Undefined</attribute>
<attribute name="mappingnumber" type="int16">2</attribute>
<attribute name="destinationname" type="string">IMPRecent</attribute>
<attribute name="autogenerateexpression" type="bool">1</attribute>
<relationship name="mappingmodel" type="1/1" destination="XDDEVMAPPINGMODEL" idrefs="z113"></relationship>
<relationship name="attributemappings" type="0/0" destination="XDDEVATTRIBUTEMAPPING" idrefs="z110 z104 z107"></relationship>
<relationship name="relationshipmappings" type="0/0" destination="XDDEVRELATIONSHIPMAPPING"></relationship>
</object>
<object type="XDDEVATTRIBUTEMAPPING" id="z103">
<attribute name="name" type="string">segments</attribute>
<relationship name="entitymapping" type="1/1" destination="XDDEVENTITYMAPPING" idrefs="z106"></relationship>
</object>
<object type="XDDEVATTRIBUTEMAPPING" id="z104">
<attribute name="name" type="string">draft</attribute>
<relationship name="entitymapping" type="1/1" destination="XDDEVENTITYMAPPING" idrefs="z102"></relationship>
</object>
<object type="XDDEVATTRIBUTEMAPPING" id="z105">
<attribute name="name" type="string">nodisturb</attribute>
<relationship name="entitymapping" type="1/1" destination="XDDEVENTITYMAPPING" idrefs="z114"></relationship>
</object>
<object type="XDDEVENTITYMAPPING" id="z106">
<attribute name="sourcename" type="string">IMPChatSession</attribute>
<attribute name="mappingtypename" type="string">Undefined</attribute>
<attribute name="mappingnumber" type="int16">3</attribute>
<attribute name="destinationname" type="string">IMPChatSession</attribute>
<attribute name="autogenerateexpression" type="bool">1</attribute>
<relationship name="mappingmodel" type="1/1" destination="XDDEVMAPPINGMODEL" idrefs="z113"></relationship>
<relationship name="attributemappings" type="0/0" destination="XDDEVATTRIBUTEMAPPING" idrefs="z109 z108 z103"></relationship>
<relationship name="relationshipmappings" type="0/0" destination="XDDEVRELATIONSHIPMAPPING"></relationship>
</object>
<object type="XDDEVATTRIBUTEMAPPING" id="z107">
<attribute name="name" type="string">lastChatTime</attribute>
<relationship name="entitymapping" type="1/1" destination="XDDEVENTITYMAPPING" idrefs="z102"></relationship>
</object>
<object type="XDDEVATTRIBUTEMAPPING" id="z108">
<attribute name="name" type="string">contactID</attribute>
<relationship name="entitymapping" type="1/1" destination="XDDEVENTITYMAPPING" idrefs="z106"></relationship>
</object>
<object type="XDDEVATTRIBUTEMAPPING" id="z109">
<attribute name="name" type="string">unreadCount</attribute>
<relationship name="entitymapping" type="1/1" destination="XDDEVENTITYMAPPING" idrefs="z106"></relationship>
</object>
<object type="XDDEVATTRIBUTEMAPPING" id="z110">
<attribute name="name" type="string">contactID</attribute>
<relationship name="entitymapping" type="1/1" destination="XDDEVENTITYMAPPING" idrefs="z102"></relationship>
</object>
<object type="XDDEVATTRIBUTEMAPPING" id="z111">
<attribute name="name" type="string">contactID</attribute>
<relationship name="entitymapping" type="1/1" destination="XDDEVENTITYMAPPING" idrefs="z114"></relationship>
</object>
<object type="XDDEVATTRIBUTEMAPPING" id="z112">
<attribute name="name" type="string">saveInContact</attribute>
<relationship name="entitymapping" type="1/1" destination="XDDEVENTITYMAPPING" idrefs="z114"></relationship>
</object>
<object type="XDDEVMAPPINGMODEL" id="z113">
<attribute name="sourcemodelpath" type="string">FMDBDemo/Models/IMPlayer.xcdatamodeld/IMPlayer.xcdatamodel</attribute>
<attribute name="sourcemodeldata" type="binary">YnBsaXN0MDDUAAEAAgADAAQABQAGDssOzFgkdmVyc2lvblgkb2JqZWN0c1kkYXJjaGl2ZXJUJHRv (...)
</attribute>
<attribute name="destinationmodelpath" type="string">FMDBDemo/Models/IMPlayer.xcdatamodeld/IMPlayer_v2.xcdatamodel</attribute>
<attribute name="destinationmodeldata" type="binary">YnBsaXN0MDDUAAEAAgADAAQABQAGDssOzFgkdmVyc2lvblgkb2JqZWN0c1kkYXJjaGl2ZXJUJHRv (...)
</attribute>
<relationship name="entitymappings" type="0/0" destination="XDDEVENTITYMAPPING" idrefs="z114 z102 z106"></relationship>
</object>
<object type="XDDEVENTITYMAPPING" id="z114">
<attribute name="sourcename" type="string">IMPChatSetting</attribute>
<attribute name="mappingtypename" type="string">Undefined</attribute>
<attribute name="mappingnumber" type="int16">1</attribute>
<attribute name="destinationname" type="string">IMPChatSetting</attribute>
<attribute name="autogenerateexpression" type="bool">1</attribute>
<relationship name="mappingmodel" type="1/1" destination="XDDEVMAPPINGMODEL" idrefs="z113"></relationship>
<relationship name="attributemappings" type="0/0" destination="XDDEVATTRIBUTEMAPPING" idrefs="z105 z112 z111"></relationship>
<relationship name="relationshipmappings" type="0/0" destination="XDDEVRELATIONSHIPMAPPING"></relationship>
</object>
</database>
databaseInfo
记录了数据库元信息,metadata中包含了持久化框架版本(CoreData)、存储版本哈希(包括涉及的Attribute、Entity、MappingModel、Property、Relation);object
包含好几种不同type
的信息,如XDDEVENTITYMAPPING
、XDDEVATTRIBUTEMAPPING
、XDDEVMAPPINGMODEL
等,XDDEVMAPPINGMODEL
包含了映射源模型及目标模型的model文件路径、model二进制数据(大概是为了分离datamodel与mappingmodel;
databaseInfo
中的meta字段sqlite中SELECT Z_PLIST FROM Z_METADATA;
比较类似;可通过以下方法获取元数据:-[[NSPersistentStoreCoordinator metadataForPersistentStoreOfType:nilURL:urlerror:]
,读取结果如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15{
NSPersistenceFrameworkVersion = 641;
NSStoreModelVersionHashes = {
IMPChatSession = <1d1f9997 5ad2eb64 ed28853b 255c3384 f4978d43 6b54119f bc37776f 9382a644>;
IMPChatSetting = <8d414f3c c71afbf6 cf2e7a9e d42e548c 24dfe5ff 8fd299a9 c57881d6 70d2ba2f>;
IMPRecent = <05593ecc 8e61b083 71b8f5a0 2c97d873 4db29dd6 5a63ffb8 968bf814 c7bf6304>;
};
NSStoreModelVersionHashesVersion = 3;
NSStoreModelVersionIdentifiers = (
""
);
NSStoreType = SQLite;
NSStoreUUID = "1AAFEF41-289B-4F22-BBD7-E05796B3B651";
"_NSAutoVacuumLevel" = 2;
}
.xcmappingmodel经Xcode编译后在.app目录中生成.cdm文件。
轻量级数据迁移
当数据模型的改变仅是实体或属性重命名、增删实体属性、属性可选特性更改或定义默认值等改变时(所有支持类型看这里),使用轻量级数据迁移即可;NSPersistentStoreCoordinator 会自动推断出mapping model(因为VersionInfo及mom中包含了足够的信息),实现代码寥寥数行:1
2
3
4
5
6
7
8
9
10
11
12
13NSError *error = nil;
NSURL *storeURL = <#The URL of a persistent store#>;
NSPersistentStoreCoordinator *psc = <#The coordinator#>;
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
BOOL success = [psc addPersistentStoreWithType:<#Store type#>
configuration:<#Configuration or nil#> URL:storeURL
options:options error:&error];
if (!success) {
// Handle the error.
}
只需要添加持久化存储时添加选项支持自动推断mappingModel进行数据迁移即可。虽然可以手动迁移数据,苹果还是推荐尽可能利用自动推断方式进行迁移:
A further advantage of using lightweight migration—beyond the fact that you don’t need to create the mapping model yourself—is that if you use an inferred model and you use the SQLite store, then Core Data can perform the migration in situ (solely by issuing SQL statements). This can represent a significant performance benefit as Core Data doesn’t have to load any of your data. Because of this, you are encouraged to use inferred migration where possible, even if the mapping model you might create yourself would be trivial.
另外,要支持lightwight的方式,Core Data必须要能在运行时找得到源模型及(迁移)目标模型,这个搜索路径默认是[NSBundle allBundles]
以及[NSBundle allFrameworks]
,模型文件存放在此外的需要使用Migration manager,见此处说明。
自定义数据迁移
当模型修改较多导致NSMigrationManager
无法推断出mapping model时,Core Data需要我们明确提供mapping model才能完成数据迁移。
创建映射模型(mapping model)
创建mapping model其实很简单,当你新建了model版本后,可以创建一个mapping model的文件,需要你确认指定映射模型的基准模型(也就是上一版本的Model)及目标模型(当前版本的Model)。
需要注意进行渐进式数据迁移,也就是每次模型版本更新后都需要创建一个mapping model。否则如果用户跳跃版本更新,会缺失部分版本之间数据迁移的映射模型,这可能是灾难性的错误;而如果要创建所有不同版本之间的映射关系,O(n^2)的复杂度,这是另一个灾难。
迁移过程
先看下数据迁移的入口,入参主要是数据库的路径及数据模型:
1 | // 迁移入口 |
简单来说,需要递归进行版本v
和版本v+1
之间的数据迁移。具体包括:
1、检查当前数据版本与模型版本的兼容性
2、查找与当前数据版本v兼容的模型版本v,以及从该模型版本v到下一模型版本v+1的映射模型mapping model,这是自动查找的
3、是否有指定的mapping model
4、构建NSMigrationManager,执行迁移
5、数据备份及继续递归迁移
1 | // CoreDataMigrationManager.m |
// 注:参考Martin Hwasser的代码,具体可见代码仓库。
迁移策略
对于更复杂的迁移,比如需要在实体之间新建关系等,则需要定制迁移策略了。定制迁移策略可以继承NSEntityMigrationPolicy
,并重载-createDestinationInstancesForSourceInstance:entityMapping:manager:error:
方法。
就如同Model
与Entity
的关系,NSMappingModel
中包含了多个NSEntityMapping
,NSEntityMapping
是具体的实体映射的描述。NSEntityMapping
在Xcode面板中可以配置User Info,这里可以做一些自定义的标识定义。
方法-createDestinationInstancesForSourceInstance:entityMapping:manager:error:
中,我们可以根据需要创建实体的实例以及手动构建关系:
1 | - (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sourceInstance |
对于需要建立关系的迁移策略而言,还需要考虑实体迁移的顺序,否则在建立关系时,对应的实体还未迁移无法建立关系。实体迁移的顺序其实就是MappingModel中的EntityMapping的次序,按需修改便可。
从上面的流程可以看到,Core Data
自定义数据迁移实际还是比较复杂的过程。希望客户端完善、好用的关系数据库ORM能早日出现。
参考资料:
Author: Jason
Permalink: http://blog.knpc21.com/ios/core-data-versioning/
文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。
Comments