数据模型(Data Model)
前面文章中介绍了Core Data堆栈,它是Core Data的核心部分。并且了解了Core Data堆栈中管理对象上下文(managed object context),持久化存储协调者(persistent store coordinator),管理对象模型(managed object model)等组成部分。这篇文章我们将集中了解Core Data中的数据模型(data model)。
在我们的应用中,管理对象模型(NSManagedObjectModel)将允许 Core Data映射持久化存储的记录到管理对象。该模型是实体描述(NSEntityDescription实例对象)的集合。
Core Data数据模型(储存在 *.xcdatamodel文件里)中定义了数据类型 (在 Core Data里的“实体”中)。大多数情况下,我们更偏向通过 Xcode 的图形界面去定义一个数据模型,但同样我们可以使用纯代码去完成这个工作。首先,你需要创建一个 NSManagenObjectModel对象,然后创建 NSEntitiyDesciption对象来表示一组实体,该实体通过 NSAttributeDescription和 NSRelationshipDescription对象来表示实体属性和实体之间的关系。
1:数据模型编辑器(Data Model Editor)
首先打开我们自己之前创建的工程,打开Xcode,然后在Project Navigator中找到Core_Data_Stack.xcdatamodeld。并点击选中,Xcode将自动显示数据模型编辑部分(Data Model Editor),如下图所示:
2:实体(Entities)
首先我们需要创建一个实体,在数据模型编辑器的底部,我们可以看到一个Add Entity的按钮,表示添加实体,Xcode会自动为实体命名为Entity,并且显示在数据模型编辑器左边区域ENTITIES部分,我们点击按钮添加一个实体,如下图:
那么实体是什么呢?Entity:是Core Data中的一个类,我们可以把实体类比为数据库中的表(其实也可以简单类比理解为OC中的类模型,我们自定义的类模型中将包含我们自己需要的属性。同样,实体类也将包含我们所需要的属性),当我们选中实体的时候,我们可以看到在右边区域会显示有attributes(属性), relationships(关系), fetched properties(获取属性).如上图所示,非常的明显。
3:属性(Attributes)
一旦我们创建了某个实体,我们就需要去定义该实体的一些属性,属性定义是非常简单的。首先我们将实体名称改为Person,可以选择双击实体可以进行修改名称,也可以在最右边的实体可编辑区域进行编辑修改,找到编辑区域中的Name部分进行修改实体名称。现在我们为Perosn实体添加属性,在Attributes区域的底部有"+"和"-"俩个按钮,很好理解,添加属性,去除属性,点击"+"添加我们的第一个属性,并且双击属性修改名称为first,点击type选择String类型,现在我们的Person实体就拥有了它自己的第一个属性,first属性。如下图所示,我们能够看到自己添加的属性:
如果我们对比数据库中的表,那么Person表就拥有了一列String类型的first。事实,当我们在应用中使用SQLite数据库的时候,Core Data将创建表来存储Person实体中的数据。但是要注意:我们强大的Core Data并不是数据库。接下来再为Person实体添加几个属性,String类型的last属性,Integer16类型的age属性。
Attribute Options
正如我前面提到的可以在右边区域修改实体的名称,那么实体的属性自然也就可以在数据模型编辑(Data Model Inspector)区域进行配置。我们选中Person实体的first属性,并且打开右侧的编辑区域,Data Model Inspector允许我们配置选中的属性,目前我们先了解一些基本的选项配置,如图所示:
瞬态(Transient)
该选项标志着属性的值是否会保存到持久化文件。如果选中表示属性不会存储到持久化存储文件中。Transient通常用在属性的值是由其他属性计算或合成而来(比如fullName属性是根据Persion类实例的firstName和lastName拼接而来)。
被声明为 transient的属性除了不被持久化到本地之外,其余所有行为都与正常属性类似。这也意味着可以对它们进行校验,撤销管理,故障处理等操作。当你将更复杂的数据模型逻辑映射到managed object subclasses的时候,transient属性将会发挥出它的优势。
默认的/可选的 (Optional)
看名称就知道是可选,对的,就是我们swift中的可选类型。每个属性可被定义成可选的或者非可选(必须)的.当我们选中Optional的时候,意味着属性可以为空。在我们接下来的例子中,我们想确保每一个Person记录都有第一个名字,所以我们取消Optional部分的选中。注意了:新的属性,默认都是可选类型的。
但是同时我们要明白,我们取消了可选,那么该属性就是必须要有值。如果我们在保存Person实体记录的时候,我们的first属性没有有效值,那么Core Data将抛出错误,这就意味着我们需要在保存之前,要确保first属性是被设置了,有值。
索引(Index)
表示底层的持久化存储文件应该为该属性生成一个索引;如果使用基于多个属性的查询条件来提取对象,指定此类属性为Indexed,可以大幅提高提取速度。但是有利有弊,索引在提高读取速度的同时却降低了写入速度,因为每当数据变更的时候,索引就需要进行相应的更新。
当把一个属性设置为 indexed时,它将在SQLite中所对应的表的列中建立索引。我们能够为任何属性创建索引,但是请留意对写性能的潜在影响。Core Data当然也支持创建复合索引(在 entity的检查器的 Indexs部分中),就像那些横跨了多个属性的索引。当你在多属性的场景下使用复合索引来获取数据时可以对检索效率进行提升。
属性类型(Attribute Type)
属性类型是非常重要的,它将告诉Core Data以什么格式保存属性并且以什么格式恢复数据。属性类型有一系列可选的类型,如果我们改变 first的属性类型为Date,我们将看到属性的配置区域将显示为Date类型。如图所示:
默认值(Default Value)
除了Transformable和Binary Data类型以外,Default适用于所有属性类型。比如:String和Date类型,将有默认值(Default Value)区域可以进行设置。这一点是比较便利的,如果一个属性被要求在插入数据库之前必须有一个有效值,那么我们可以使用该属性。
注意:默认值只是当记录被创建的时候使用,然后我们对于存在的 Person实体记录,当我们更新设置了first属性为nil时,Core Data将不会使用默认值为first属性赋值的,相反Core data将抛出错误,因为我们已经标记first属性为必须有值。
正则(Reg.Ex)
Reg.Ex是Regular Expression的缩写,主要是用来验证属性值是否匹配特定的模式。此选项只对String类型有效。
验证(Validation)
Validation可以保证非法数据不被保存进持久化存储文件中。数值属性类型(Integer 16/32/64、Float、Double、Decimal)都有maximum和minimum最大值最小值设定。你也可以对String类型设置最大长度和最小长度。或对Date类型设置日期范围。不过最好的做法是当用户设置数据时就开始验证数据,而非等到向上下文发送save消息才验证数据。
允许外部存储(Allows External Storage)
当属性类型选择二进制数据时,将有该项可选择。Allows External Storage允许大尺寸的二进制数据可以保存在持久化存储文件的外部。当你保存如照片,音频或视频时,建议是选中该选项,这样Core Data就会对大于1MB的数据保存在持久化存储文件的外部。试想想,如果数据太大,那么加载数据的时候耗时将延长,那么肯定会对性能有影响,我们并不想要这样的结果,所以Core Data为我们提供了遍历的选择操作。如下图所示:
关系(Relationships)
关系分为三种:one to one 一对一、one to many 一对多、many to many多对多。关系还有一个属性是Inverse,除了特殊情况,是一定要选的,这个属性表示了对应关系。也是CoreData进行对象图维护的依据。 从OC的角度讲,一对一关系类似某个变量实例保存了指向另一个OC类实例的指针,而一对多关系则如同保留了一个指向诸如NSMutableArray或者NSSet这样的集合类的指针,它们可以容纳多个对象。
当我们开始工作的两个实体之间有关系的时候,Core Data将非常好用,我们先添加第二个实体,命名为Address,并且为Address实体添加四个String类型的属性,分别为:street, number, city, country,方法步骤跟之前一样。
两个实体之间的关系有着一系列的特性,比如:name,destination,the cardinality of the relationship,反转关系(inverse relationship,),和关系的删除规则(relationship's delete rule).通过为Person和Address两个实体建立关系,来学习更多关系(Relationships)的详情:
Name, Destination, and Optionality
现在我们选中Person实体,点击Relationships区域下的"+"按钮,添加关系,relationship下对应的名称为address,并且设置Destination为Address实体。这表示每一个person记录都能够与address相关联(类比理解就是,一个模型类中有另一个模型类的对象属性)。如下图所示:
跟属性(attributes)一样,实体间的关系(relationships)默认也是可选(optional)的。这意味着如果person记录与address记录没有关系也不会有错误抛出,我们改一下,去除选中optional。
反转关系(Inverse Relationship)
现在,person有与address记录建立关系。person有一个address记录相联系,但是address记录并不知道person记录,因为我们当前建立的关系是单方向的从Person实体到Address实体。在Core Data中有大多数的关系都是双向的,实体相互知道存在的关系。所以现在我们创建一个反转(Inverse)关系,从Address实体到Person实体。所以选中Address实体,命名为person,Destination对应Perosn实体。如下图:
虽然我们创建了Address实体和Person实体之间的反转关系,但是Xcode给了我们一个警告,如图所示:
Person.address should have an inverse 和 Address.person should have an inverse两个警告。这是为什么呢?因为Core Data并不能够知道这么多关系中,哪一个关系是反转关系。这很容易解决,选中Person实体并且设置Inverse关系为person,及表示在 perosn建立的关系中,有了address到person的反转。如果我们现在选中Address实体,我们将看到address也建立了反转关系,为person。如图所示:
数据模型图(Data Model Graph)
当数据模型(data model)变得复杂之后,实体之间的关系将容易变得混乱和不清楚。幸运的是,Xcode已经为我们考虑了这一点,数据模型编辑器有两种样式,表格(table)和图形(graph),在编辑器的右边底部,我们能够看到Editor Style等字和对应的按钮,是的,就是它,这可以让我们选择切换两种模式,点击graph style,我们将看到如下:
在图形样式下,我们将看我们自己目前创建的对象图形,它将告诉我们所创建的实体,实体所拥有的属性,实体之间的关系,一个最有用的特性就是,这是一种视觉化的方式呈现实体在数据模型中的关系。连接Person实体和Address实体之间的线并带有箭头,指明了实体的关系,我们可以看到相互指向,即双向关系。
一对多关系(To-Many Relationships)
目前我们所建立的关系是单一关系(to-one relationships),一个person能够有一个address,一个地址属于一个person。然后,也很有可能,许多人居住在相同的地址,那么我们怎么描述这种关系呢?
其实很容易,我们现在就改变person的关系,将建立与Adrress实体之间一对多(to-many relationship)的关系.选中person,修改名称为persons,命名为persons表明有多个,你懂得!在右边的编辑区域选择type为To Many,如下图所示:
关系的命名不重要,它只是表示一对多的关系,别人看到更容易理解。注意:现在的数据模型图形中已经自动更新了。之前的单箭头方向变成了双箭头,很好的体现了相互之间的关系。
多对多关系(Many-To-Many Relationships)
许多人可以居住在同一个地址,那么一个人可以有多个地址吗?那是必须的啊!比如:家庭住址,工作地址等。Core Data同样为我们解决了这个问题,通过创建many-to-many的关系。我们现在选中Person实体中的address关系,修改名字为addresses,并且设置tyep类型为To Many,现在我们的数据模型图形又自动更新了,可以看到实体之间都是双箭头。如下图:
Core Data建立实体之间的关系是非常便利的,在关系(relationship)中的目的(destination)实体能够使用相同的,这就是反身关系(reflexive relationship).很有可能相同的类型与不同的名称与多个关系。
Delete Rules
如果在record中,关系的一端被删除了,那会怎么样呢?假设我们有一个Account实体与一个User实体的关系是 to-many relationship。换句话说。一个账号(account)可能有许多的用户(users),每一个用户(user)属于一个账号。如果我们删除了一个用户,或者删除了一个账号,那么会怎么样?在Core Data中,每种关系都有删除规则,有了它删除问题就是小case。
我们并不需要担心当记录被删除的时候,进行明确的更新持久化存储。 Core Data将处理这个问题,并且确保对象图形始终保持一致状态。接下来,回到工程。选中Person实体中的addresses关系,并打开右边编辑区域,我们将看到Delete Rule,该类型包含了4种选中, No Action, Nullify, Cascade, and Deny.如图所示:
No Action
如果选中No Action,Core Data并不会更新或者进行通知原有记录的关系。这也意味着在关系中的原记录仍然认为它与被删除的记录有着关系。这并不是我们想要的。
Nullify
当destination被删除的时候。该选项将设置relationship中的destination为null。这是关系默认的删除规则,当一个关系是可选的时候,我们能够使用这种删除规则。
Cascade
如果Person到Address的关系设置为Cascade,当删除一条person记录时,也将删除与person记录相关的任意address记录。这是非常有用的,例如,如果关系被要求必须存在,如果没有关系记录就不能够存在。好比如,一个用户,如果没有相关联的账号,那么关系就不应该存在。
Deny
在某种意义上,Deny是Cascade的反转。例如:如果我们有一个账号(Account)实体,跟一个用户(User)实体有着to-many relationship,并且删除规则设置为Deny。一个账号记录仅仅当没有用户记录相关联的时候才能够删除。这确保了没有用户记录就没有账户记录。
实体继承(Entity Inheritance)
实体继承跟类继承是非常相像的,同样非常有用。如果我们有一些实体,它们比较相似,即实体中有些属性是一样的。那么我们可以创建一个父实体,把共同的属性在父实体中声明。而不是在每个具体的实体中,重复添加相同的属性。所以,只要我们创建父实体,然后,子实体继承于父实体即可。举个例子:我们定义一个Person实体,做为父实体。拥有firstName和lastName两个属性,现在有添加了另外两个子实体 Employee和Customer实体,这两个实体,同样需要firstName和lastName两个属性,我们只需要继承Person实体即可。下图显示了继承关系的图形布局:
注意:实体继承是与使用SQLite进行持久化存储一起工作的,所有继承于父实体的子实体都将存在SQLite相同的表中。这个因素在SQLite持久化存储的时候可能存在性能问题。
补充属性类型:
关于Core Data属性类型,玉令博客中一段比较全面的解释:
1: Undefined选项值是新创建的属性的默认类型;如果属性类型为undefined,项目将无法通过编译。
2: Integer 16/32/64只表示整数,没有小数点。所以如果10除以3,你将会得到3,而余数1会丢失。Integer 16/32/64之间唯一的区别是所表示的数值范围不同。因为Core Data使用符号数,所以起始范围是负数,而不是0。
Integer 16 数值范围:-32768~32767;
Integer 32 数值范围:-2147483648~2147483647;
Integer 64 数值范围:–9223372036854775808~9223372036854775807。
标准整型数的最大值和最小值可以在stdint.h中找到。在任何类文件中输入INT32_MAX,选中右击,然后选择Jump To Definition,你将看到许多最大值最小值定义。实体的属性的类型是Integer 16/32/64,当创建此实体对应的NSManagedObject子类时,属性最终的类型将会是NSNumber。
3:Double和Float可以认为是有小数部分的整数。它们都是基于二进制数值系统,在CPU运算时很可能会发生舍入误差。比如1/5,如果使用十进制数值系统,可以精确表示为0.2.但在二进制数值系统中,只能表示一个大概,在小数部分你会得到大量数字。所以不要使用Integer、Double、Float表示货币值。计算精度越高则越加趋于准确值,但内存占用也会越大。一个Float数使用32bit进行存储,一个Double数使用64bit。它们都使用科学计数法进行存储,所以一个数包含尾数和指数部分
在iOS中,最大的Float值是340282346638528859811704183484516925440.000000,最小的Float值是340282346638528859811704183484516925440.000000Double和Float都有一个符号位。而Double比Float的数值范围更大。
当你决定该选择Float还是Double时,想一下你的属性是否真的需要超过Float提供的7位精度,如果不是,你应该选择Float,因为它更加匹配64bit的iPhone 5S底层处理器。除此之外,如果你想增加浮点数的计算速度而精度并没有严格要求,Float也是最佳选择。实体的属性的类型是Float或Double,当创建此实体对应的NSManagedObject子类时,属性最终的类型将会是NSNumber。
4:Decimal(十进制)是处理货币值和其他需要十进制场合下最佳选择,Decimal提供了优秀的计算精度,也消除了计算过程中的舍入误差。因为CPU的本地数制是二进制,所以CPU在处理十进制数时,开销会多一点。实体的属性的类型是Decimal,当创建此实体对应的NSManagedObject子类时,属性最终的类型将会是NSDecimalNumber。当你使用NSDecimalNumber执行计算时(如加减乘除计算),为了保证计算精度,你只能使用它提供的内建方法。更多关于NSDecimalNumber可参见这里。
5:String类型和Objective-C中的NSString类似,用于保存字符数组。当生成实体对应的NSManagedObject子类时,String属性被表示为NSString。
6:Boolean数据类型被用于表示YES/NO值。当生成实体对应的NSManagedObject子类时,Boolean数据类型会被表示为NSNumber。所以为了获取布尔值,你需要想NSNumber对象发送boolValue消息。
7:Date类型是自解释类型。用来存储日期和时间。当生成实体对应的NSManagedObject子类时,Date类型会被表示为NSDate。
8:Binary Data用来表示照片,音频,或一些BLOB类型数据(“Binary Large OBjects” such as image and sound data)。当生成实体对应的NSManagedObject子类时,Binary Data数据类型会被表示为NSData。
9:Transformable属性类型用于存储一个Objective-C对象。该属性类型允许你存储任何类的实例,比如你使用Transformable属性表示UIColor。当生成NSManagedObject子类时,Transformable类型会被表示为id。对于id对象的保存和解档需要使用一个NSValueTransformer的实例或子类的实例。由该类负责属性值与NSData之间的转换。但这也相当的简单,尤其是当属性值的类型已经实现了NSCoding协议,此时系统会自动提供一个默认的NSValueTransformer实例来完成归档和解档。
相关优秀文章:
http://objccn.io/issue-4-4/
http://yulingtianxia.com/blog/2014/05/02/chu-shi-core-data-2/