16 coredata

50
Core Data 范圣刚,[email protected] , www.tfan.org

Upload: tom-fan

Post on 12-May-2015

493 views

Category:

Technology


2 download

DESCRIPTION

iOS Core Data:关系对象模型

TRANSCRIPT

•保存和加载数据• Local or remote?

• Archiving or Core Data?

• Archiving: 对整个⽂文件进⾏行操作

• Core Data: 操作存储对象的⼦子集

•性能

对象-关系映射(ORM)

• Core Data 是提供了 object-relational mapping 的⼀一个框架,可以把 Objective-C 对象转成存储在 SQLite 数据库⽂文件的数据,反之亦然。

• Core Data 提供了⼀一种可以提取和存储数据到关系数据库⽽而不需要了解 SQL 的能⼒力。

•我们这⼀一个章节在 Homepwner 的 BNRItemStore 中⽤用 Core Data 替换掉原来的 keyed archiving。

对 Homepwner 使⽤用Core Data

•我们现在的 Homepwner 应⽤用使⽤用 archiving 来保存和加载数据,对于数据量较⼩小的情况还可以,但是对于数据量⾮非常⼤大的情况下,我们可能就想要能够增量提取和更新数据的 Core Data。

•⾸首先,我们还是要把 Core Data framework 加⼊入我们的项⺫⽬目。

•选择 Homepwner target,在 Build Phases 下⾯面,打开 Link Binary with Libraries, 加 + 号 添加 Core Data framework

模型⽂文件

Core Data ⾓角⾊色

• table/class -> entity

• columns/instance -> attributes

• BNRItem entity

•打开 Homepwner.xcodeproj

• File -> New -> File, iOS -> Core Data, Data Model, 命名为 Homepwner

•将会创建⼀一个 Homepwner.xcdatamodeld 的⽂文件

•从 project navigator 中打开这个⽂文件,我们就可以看到可以操作 Core Data model !le 的⽤用户界⾯面

•找到屏幕左下⾓角的 “Add Entity”按钮点击,⼀一个新的 Entity 将会出现在左⼿手边的 entities 列表中,命名为 BNRItem

在 Attributes 中对应的设置属性

Attribute type

itemName String

serialNumber String

valueInDollars Integer 32

dateCreated Date

imageKey String

thumbnail Binary Data

thumbnail Unde!ned

•从 Attributes 中选择 thumbnail,点击 inspector 中的 attribute inspector,勾选 Transient

•设成 Transient 是告诉 Core Data 我们的 thumbnail 将在运⾏行时创建,⽽而不是从⽂文件保存和加载。

• orderingValue -> Double

再增加⼀一个⽤用于排序的属性

relationship

•⺫⽬目前模型⽂文件对于保存和加载 items 已经⾜足够了

•我们再增加⼀一个新的名为 BNRAssetType 的实体,⽤用于描述 items 的分类。

•这样就构造出⼀一个实体间的 relation,演⽰示 Core Data 实体间关系的功能。

•再添加⼀一个名为 BNRAssetType 的实体⽂文件

•增加⼀一个叫做 label 的属性,类型是 String,把它作为 items 分类的名字

•现在我们需要来建⽴立 BNRAssetType 和 BNRItem 之间的关系

Homepwner 中的实体

•给模型⽂文件增加 relationships。

•选择 BNRAssetType 实体,点击 Relationship 部分的 + 号。

•在 Relationship 列把这个 relation 命名为 items;

•然后从 Destination 列中选择 BNRItem;

•在 data model inspector 中,勾选 To-Many Relationship

•给 BNRItem 实体增加⼀一个名为 assetType 的关系,把 BNRAssetType 作为⺫⽬目的,在 Inverse 列,选择 items。

NSManagedObject 及其⼦子类

•当⼀一个对象被提取出来时,默认类型是 NSManagedObject,是 NSObject的⼀一个⼦子类,知道如何跟 Core Data 互相操作。

•NSManagedObject 类似字典:持有实体中所有属性的⼀一个 key-value pair。

•NSManagedObject 差不多就是⼀一个容器。如果我们想让模型对象做更多⼯工作,我们必须⼦子类化 NSManagedObject。

•选中 BNRItem 实体,显⽰示 data model inspector,并更改 Class 字段为 BNRItem。

•现在当 BNRItem 实体被提取出来时,这个对象的类型将会是 BNRItem。

•在 Finder 中,⾸首先备份好我们的 BNRItem.h 和 BNRItem.m ⽂文件

•然后在 Xcode 中把这两个⽂文件从 project navigator 中删除。

•重新打开 Homepwner.xcdatamodeld

•选择 BNRItem 实体

•然后选择从 New 菜单中选择 File,iOS -> Core Data, 选择 NSManagedObject subclass 选项,提⽰示保存的时候,勾选“Use scalar properties for primitive data types”

• Xcode 将会⽣生成两个新的 BNRItem.h 和 BNRItem.m ⽂文件

•打开 BNRItem.h, 把 thumbnail 属性的类型改成 UIImage

•然后增加⼀一个跟之前⼀一样的⼀一个⽅方法声明@interface BNRItem : NSManagedObject

@property (nonatomic) int32_t itemName;@property (nonatomic, retain) NSString * serialNumber;@property (nonatomic) int32_t valueInDollars;@property (nonatomic) NSTimeInterval dateCreated;@property (nonatomic, retain) NSString * imageKey;@property (nonatomic, retain) NSData * thumbnailData;//@property UNKNOWN_TYPE UNKNOWN_TYPE thumbnail;@property (nonatomic, strong)UIImage *thumbnail;@property (nonatomic) double orderingValue;@property (nonatomic, retain) NSManagedObject *assetType;

- (void)setThumbnailDataFromImage:(UIImage *)image;

@end

•NSDate 变成了 NSTimeInterval

•打开我们的 DetailViewController.h, 定位到 viewWillAppear:, 替换下⾯面的代码

// [dateLabel setText:[dateFormatter stringFromDate:[item dateCreated]]]; // 把 time interval 转换成 NSDate NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:[item dateCreated]]; [dateLabel setText:[dateFormatter stringFromDate:date]];

•然后,从旧的 BNRItem.m 中拷⻉贝 setThumbNailFromImage: ⽅方法到新的⽂文件

- (void)setThumbnailDataFromImage:(UIImage *)image{ CGSize origImageSize = [image size]; // thumnail 的矩形⼤大⼩小 CGRect newRect = CGRectMake(0, 0, 40, 40); // 计算缩放⽐比 float ratio = MAX(newRect.size.width / origImageSize.width, newRect.size.height / origImageSize.height); // ⽣生成⼀一个带缩放因⼦子的透明位图上下⽂文 UIGraphicsBeginImageContextWithOptions(newRect.size, NO, 0.0); // ⽣生成⼀一个圆⾓角矩形路径 UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:newRect cornerRadius:5.0]; // 后续绘制 clip 到这个圆⾓角矩形 [path addClip]; // 图⽚片放到缩略图中间 CGRect projectRect; projectRect.size.width = ratio * origImageSize.width; projectRect.size.height = ratio * origImageSize.height; projectRect.origin.x = (newRect.size.width - projectRect.size.width) / 2.0; projectRect.origin.y = (newRect.size.height - projectRect.size.height) / 2.0; // 把图⽚片绘制上来 [image drawInRect:projectRect]; // 从图⽚片上下⽂文获得图⽚片,作为我们的缩略图保存 UIImage *smallImage = UIGraphicsGetImageFromCurrentImageContext(); [self setThumbnail:smallImage]; // 得到该图⽚片 PNG 形式,把它作为我们可以 archive 的数据 NSData *data = UIImagePNGRepresentation(smallImage); [self setThumbnailData:data]; // 完成以后,清除图⽚片上下⽂文资源 UIGraphicsEndImageContext();}

•在 BNRItem.m 中重写 awakeFromFetch ⽅方法,来从 thumbnailData 设置 thumbnail

•使⽤用 archiving 的时候我们是在 initWithCoder: 时做的

- (void)awakeFromFetch{ [super awakeFromFetch]; UIImage *tn = [UIImage imageWithData:[self thumbnailData]]; [self setPrimitiveValue:tn forKey:@"thumbnail"];}

•我们创建⼀一个新的 BNRItem 实例时,它将会被加⼊入数据库。

•当对象被添加到数据库时,它将被发送 awakeInsert 消息

•在 BNRItem.m 中实现 awakeFromInsert

•原先我们是在 BNRItem 的 designated initializer 中添加⼀一些附加⾏行为的

- (void)awakeFromInsert{ [super awakeFromInsert]; NSTimeInterval t = [[NSDate date] timeIntervalSinceReferenceDate]; [self setDateCreated:t];}

更新 BNRItemStore

BNRItemStore 和 NSManagedObjectContext

•在 BNRItemStore.h 中,导⼊入 Core Data 然后增加三个实例变量

#import <Foundation/Foundation.h>// 导⼊入 Core Data 头⽂文件#import <CoreData/CoreData.h>

@class BNRItem;

@interface BNRItemStore : NSObject{ NSMutableArray *allItems; NSMutableArray *allAssetTypes; NSManagedObjectContext *context; NSManagedObjectModel *model;}

•在BNRItemStore.m 中,更改 itemArchivePath 的实现来返回⼀一个不同的路径供 Core Data 存储数据

- (NSString *)itemArchivePath{ NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentDirectory = [documentDirectories objectAtIndex:0]; // return [documentDirectory stringByAppendingPathComponent:@"items.archive"]; return [documentDirectory stringByAppendingPathComponent:@"store.data"];}

• BNRItemStore 被初始化时,它需要设置 NSManagedObjectContext 和 NSPersistentStoreCoordinator

•我们需要创建⼀一个 NSManagedObjectModel 来持有 Homepwner.xcdatamodeld 的实体信息,并且使⽤用这个对象初始化 persistent store coordinator

•因此我们将创建 NSManagedObjectContext 的实例,并且指定它使⽤用这个 persistent store coordinator 来保存和加载对象

•在 BNRItemStore.m 中更新 init- (id)init{ self = [super init]; if (self) {//// allItems = [[NSMutableArray alloc] init];// NSString *path = [self itemArchivePath];// allItems = [NSKeyedUnarchiver unarchiveObjectWithFile:path];// // 如果数组之前没有被保存,创建⼀一个新的空数组// if (!allItems) {// allItems = [[NSMutableArray alloc] init];// } // 读取 Homepwner.xcdatamodeld model = [NSManagedObjectModel mergedModelFromBundles:nil]; NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model]; // SQLite ⽂文件在哪⼉儿? NSString *path = [self itemArchivePath]; NSURL *storeURL = [NSURL fileURLWithPath:path]; NSError *error = nil; if (![psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) { [NSException raise:@"Open failed" format:@"Reason: %@", [error localizedDescription]]; } // ⽣生成 managed object context context = [[NSManagedObjectContext alloc] init]; [context setPersistentStoreCoordinator:psc]; // 不需要管理 undo [context setUndoManager:nil]; } return self;}

•之前我们使⽤用 keyed archiving 的时候,当我们要求保存数据,BNRItemStore 将会写⼊入整个 NSMutableArray 的 BNRItems。

•现在我们让它给 NSManagedObjectContext 发送 save: 消息。Context 将会更新 store.data 中从 后⼀一次保存其有任何变更的所有记录。

•在 BNRItemStore.m 中,更改 saveChanges

@implementation BNRItemStore- (BOOL)saveChanges{// // 返回成功或失败// NSString *path = [self itemArchivePath];// return [NSKeyedArchiver archiveRootObject:allItems toFile:path]; NSError *err = nil; BOOL successful = [context save:&err]; if (!successful) { NSLog(@"Error saving: %@", [err localizedDescription]); } return successful;}

NSFetchRequest 和 NSPredicate

•要从 NSManagedObjectContext 提取数据,我们需要 prepare and execute ⼀一个 NSFetchRequest。

•当这个 fetch 请求被执⾏行以后,我们将得到⼀一个符合这个请求的 parameters 的所有对象的⼀一个数组

• fetch 请求需要我们想要从中获取对象的⼀一个实体的描述。这⾥里我们指定 BNRItem 实体。

•也可以设置请求的 sort descriptors 来指定数组中对象的顺序。

•⼀一个 sort descriptor 具有⼀一个映射到⼀一个 attribute 的 key,和⼀一个表⽰示正序还是倒序的 BOOL 值

•我们希望使⽤用使⽤用 orderingValue 正序排列返回的 BNRItem

•在 BNRItemStore.h 中,声明⼀一个新的⽅方法- (void)loadAllItems;

•在 BNRItemStore.m 中定义 loadAllItems 来 prepare and execute ⼀一个 fetch 请求,并且保存结果到 allItems 数组

- (void)loadAllItems{ if (!allItems) { NSFetchRequest *request = [[NSFetchRequest alloc] init]; NSEntityDescription *e = [[model entitiesByName] objectForKey:@"BNRItem"]; [request setEntity:e]; NSSortDescriptor *sd = [NSSortDescriptor sortDescriptorWithKey:@"orderingValue" ascending:YES]; [request setSortDescriptors:[NSArray arrayWithObject:sd]]; NSError *error; NSArray *result = [context executeFetchRequest:request error:&error]; if (!result) { [NSException raise:@"Fetch failed" format:@"Reason: %@", [error localizedDescription]]; } allItems = [[NSMutableArray alloc] initWithArray:result]; }}

•在 BNRItemStore.m 中,在 init 的末尾发送这个消息给 BNRItemStore

// 不需要管理 undo [context setUndoManager:nil]; [self loadAllItems]; } return self;}

•要想有选择性的 fetch ⼀一些实例,我们可以添加⼀一个 predicate(⼀一个 NSPredicate)到我们的 fetch 请求,只有满⾜足这个 predicate 的对象会被返回。

• predicate 包含的是⼀一个可以为 true 或 false 的条件。例如:

// ⼀一个 predicate 的例⼦子,设定 fetch 条件 NSPredicate *p = [NSPredicate predicateWithFormat:@"valueInDollars > 50"]; [request setPredicate:p];

• predicate 也可以被⽤用来过滤⼀一个数组的内容,例如 // predicate ⽤用来过滤数组的例⼦子 NSArray *expensiveStuff = [allItems filteredArrayUsingPredicate:p];

增加和删除 items

•要创建⼀一个新的 BNRItem,我们将请求 NSManagedObjectContex 从 BNRItem 实体插⼊入⼀一个新的对象。它将返回⼀一个 BNRItem 的实例。

•在 BNRItemStore.m 中,编辑 createItem ⽅方法- (BNRItem *)createItem{//// BNRItem *p = [BNRItem randomItem];// BNRItem *p = [[BNRItem alloc] init]; double order; if ([allItems count] == 0) { order = 1.0; } else { order = [[allItems lastObject] orderingValue] + 1.0; } NSLog(@"在 %d 个 items 之后添加,顺序是:%.2f", [allItems count], order); BNRItem *p = [NSEntityDescription insertNewObjectForEntityForName:@"BNRItem" inManagedObjectContext:context]; [p setOrderingValue:order]; [allItems addObject:p]; return p;}

•当⼀一个⽤用户删除了⼀一个 item 的时候,我们必须通知 context,这样它也会被从数据库删除。

•在 BNRItem.m 中,增加下列的代码到 removeItem:

- (void)removeItem:(BNRItem *)p{ // BNRItem 被从 store 中移除的时候,它的 image 也应该被从⽂文件系统删除 NSString *key = [p imageKey]; [[BNRImageStore sharedStore] deleteImageForkey:key]; // 增加通知 context 有数据被删除的代码 [context deleteObject:p]; [allItems removeObjectIdenticalTo:p];}

重排 items

•我们需要为 BNRItem 替换的 后⼀一点功能是在 BNRItemStore 中重新排序 BNRItems。

•因为 Core Data 不会⾃自动化的处理排序,每次它在 table view 中被移动时我们必须更新 BNRItem 的 orderingValue。

•在 BNRItemStore.m 中,修改 moveItemAtIndex:toIndex: 来处理重新排序 items

- (void)moveItemAtIndex:(int)from toIndex:(int)to{ if (from == to) { return; } // 得到被移动的对象的指针,以便我们可以把它重新插⼊入 BNRItem *p = [allItems objectAtIndex:from]; // 从数组中删除 [allItems removeObjectAtIndex:from]; // 在新的位置重新插⼊入 [allItems insertObject:p atIndex:to]; // Core Data 的排序 // 为被移动的对象计算⼀一个新的 orderValue double lowerBound = 0.0; // 数组中在它之前是否有⼀一个对象? if (to > 0) { lowerBound = [[allItems objectAtIndex:to -1] orderingValue]; } else { lowerBound = [[allItems objectAtIndex:1] orderingValue] - 2.0; } double upperBound = 0.0; // 数组中在它之后是否有⼀一个对象 if (to < [allItems count] - 1) { upperBound = [[allItems objectAtIndex:to + 1] orderingValue]; } else { upperBound = [[allItems objectAtIndex:to -1] orderingValue] + 2.0; } double newOrderValue = (lowerBound + upperBound) / 2.0; NSLog(@"moving to order: %f", newOrderValue);}