在iPhone所有数据存储的方法里面,Core Data是重要数据存储的最佳选择。它能降低你应用的内存开销,提升响应速度,并把你从繁琐的代码中解脱出来。
然而,学习Core Data之路异常久远。不过这也是这一系列教程的由来 – 让你快速掌握Core Data基础知识。
作为该系列教程的第一部分,我们将为我们的对象建立一个可视化数据模型。为保证其有效性,我们会做一个快速肮脏测试(dirty test验证其健壮性和有效性),然后将其勾在一个表视图(table view)里,这样我们可以看到这一列对象。
系列教程的第二部分,我们将讨论如何将数据预先载入到Core Data中,这样我们的应用启动时就初始化好了。
最后的部分,我们将讨论如何使用NSFetchedResultsController来优化应用,达到降低内存开销以及改进响应时间的目的。
在本教程开始之前,我建议先翻阅我以前写的SQLite for iPhone Developers教程,这会让你更容易理解。另外,2个教程做的应用(app)都是一样的,只是这次我们用的是Core Data!
下面让我们开始!建立一个新的Window-based Application,勾选“Use Core Data for storage”,将工程命名为“FailedBanksCD.”
在开始前,我们快速看一下建好的工程。首先展开Resources并双击FailedBanksCD.xcdatamodel,会弹出一个可视化编辑器-这就是我们接下来会用到的模型对象图示。现在我们先把它关闭。
然后看看FailedBanksCDAppDelegate.m。在这里你会看见已经为我们实现的一些新函数,用于建立Core Data”堆”。新增了一个 managed object context,一个managed object model, persistent store coordinator. 啊??
别担心。名字虽然开始听起来奇怪,一旦你打通“任督二脉”就很容易理解到他们是什么。
无需过于担心这些方法 – 你不用过于纠缠这些。相反,了解它们是什么以及它们代表的涵义会比较好。
因此我们不禁会在Core Data中做同样的尝试。然而,使用Core Data你无法检索一个对象中的单一特性(attributes),你只能检索整个对象。但是,只要我们把这个对象分解成两块- FailedBankInfo和FailedBankDetails-我们就能实现同样的目标。
所以让我们看看如何实现。打开可视化编辑器(展开Resources并双击FailedBanksCD.xcdatamodel)。
从我们的模型中建立一个对象开始-用Core Data术语来说就是“Entity”。在左上面板,单击+号来增加一个新的实体(Entity),如下:
按下+以后,会创建一个新的实体(Entity),然后在右边的面板显示实体(Entity)的属性(properties),如下:
将实体(Entity)命名为FailedBankInfo。注意到它目前是NSManagedObject的子类。这是所有实体(Entity)的默认类,我们现在用的也是。等下我们会回来改成自定义类。
我们再来添加一些特性(attributes):首先,确保在左边面板中选中相应的实体(entity),然后在中间的面板中点击+号,选中“Add Attribute”,如下:
在右边的属性(property)面板,将特性(attribute)命名为“name”,类型设为“String”,如下:
现在,重复添加2个特性(attributes), “city”和“state”, 类型都是string.
最后,我们需要连接这2种类型。选择FailedBankInfo,单击中间面板的+号,但这次选择“Add relationship”:
把关系命名为“details”,然后设置其destination为“FailedBankDetails”。好,我们刚刚做了什么?我们在Core Data中建立了关系,把两个实体(entity)连接在了一起。在这种情况下,我们建立了一对一的关系-每个FailedBankInfo恰好对应一个FailedBankDetails。在此情景下,Core Data将设定我们的数据库使得FailedBankInfo表拥有一个字段来对应FailedBankDetails对象的ID。
Apple建议,每当你建立一个到其他对象的连接,最好也从另外一个对象建立一个反向连接。所以我们也这么做。
现在添加一个关系到“FailedBankDetails”,取名为“info”,设置其destination到“FailedBankInfo”,然后inverses到“details”.
同样,设置两个关系的delete rule为“cascade”。意思是如果你用Core Data删除一个对象,Core Data会把另外一个对象也删除。这在此处是说得通的,因为一个没有FailedBankInfo的FailedBankDetails是没有任何存在价值的。
不管你信不信,这可能是我们要做的事当中最重要的。接下来就测试下Core Data是否管用。首先,在我们的数据库中加入测试对象。打开FailedBanksCDAppDelegate.m并在applicationDidFinishLaunching最前面加入:
NSManagedObjectContext *context = [self managedObjectContext]; NSManagedObject *failedBankInfo = [NSEntityDescription insertNewObjectForEntityForName:@"FailedBankInfo" inManagedObjectContext:context]; [failedBankInfo setValue:@"Test Bank" forKey:@"name"]; [failedBankInfo setValue:@"Testville" forKey:@"city"]; [failedBankInfo setValue:@"Testland" forKey:@"state"]; NSManagedObject *failedBankDetails = [NSEntityDescription insertNewObjectForEntityForName:@"FailedBankDetails" inManagedObjectContext:context]; [failedBankDetails setValue:[NSDate date] forKey:@"closeDate"]; [failedBankDetails setValue:[NSDate date] forKey:@"updatedDate"]; [failedBankDetails setValue:[NSNumber numberWithInt:12345] forKey:@"zip"]; [failedBankDetails setValue:failedBankInfo forKey:@"info"]; [failedBankInfo setValue:failedBankDetails forKey:@"details"]; NSError *error; if (![context save:&error]) { NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]); } |
在第一行,我们通过模板的帮助取得了一个指针,该指针指向我们的管理对象上下文(managed object context)。
之后我们为FailedBankInfo实体(entity)创建一个新NSManagedObject实例,凭借的是对insertNewObjectForEntityForName的调用。Core Data存储的每个对象都继承自NSManagedObject。一旦你得到了对象的实例,你就可以调用setValue来设置你在可视化编辑器里面定义的对象的任一特性(attribute)。
接着我们为FailedBankInfo和FailedBankDetails建一个银行信息做测试。这时对象刚刚在内存中被修改 – 要存回数据库的话需要在管理对象上下文中(managedObjectContext)调用save。
我们就在这插入对象-一点儿也不需要SQL语句!
开始尝试之前,我们来加入更多代码来列举数据库现有的对象:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"FailedBankInfo" inManagedObjectContext:context]; [fetchRequest setEntity:entity]; NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error]; for (NSManagedObject *info in fetchedObjects) { NSLog(@"Name: %@", [info valueForKey:@"name"]); NSManagedObject *details = [info valueForKey:@"details"]; NSLog(@"Zip: %@", [details valueForKey:@"zip"]); } [fetchRequest release]; |
在这里我们建立一个新的对象,我们称之为fetch request. 你可以把一个获取请求看作是一条SELECT语句。我们调用entityForName来得到一个指向FailedBankInfo实体的指针,然后用setEntity来告知获取请求这就是我们想要的。
之后对管理对象上下文(managed object context)调用executeFetchRequest来拉取FailedBankInfo对象的所有表单并放在“暂存器”中。然后我们遍历NSManagedObject,并使用valueForKey来拉取各种字段。
注意到尽管我们只是拉取FailedBankInfo表单中的对象,我们仍然可以访问相关联的FailedBankDetails对象,通过访问FailedBankInfo的details属性(property)。
这是如何起作用的呢?当你访问这个属性(property)的时候,Core Data注意到当前上下文没有这一数据,然后“报错”,这一错误基本上意味着它已经跳到那个数据库并拉取了你所需的正确内容。非常方便!运行这段代码,看看你的输出窗口,每次运行的时候你都会在你的数据库中看到一个示例用银行。
我不清楚你是怎样,但是我在处理这方面事务的时候特别喜欢看到实实在在的SQL语句来观察进展(并确定事情如我所愿)
Apple再一次提供了简单的解决方案。在Xcode中打开Executables下拉列表并找到可执行的FailedBanksCD。右键并点击“Get Info”。导航到Arguments标签(tab)并增加以下语句:“-com.apple.CoreData.SQLDebug 1″。完成的结果如下:
现在运行代码,在调试输出窗口你可以看到一些有用的跟踪语句,提醒你现在发生的事情:
于是我们可以看到事情正如我们所期望的那样。前面2条select以及update语句表明Core Data正在做记录以维持实体(entity)的下一个ID。
1 2 3 4 5 6 7 8 9 10 11 | SELECT Z_VERSION, Z_UUID, Z_PLIST FROM Z_METADATA SELECT Z_MAX FROM Z_PRIMARYKEY WHERE Z_ENT = ? UPDATE Z_PRIMARYKEY SET Z_MAX = ? WHERE Z_ENT = ? AND Z_MAX = ? INSERT INTO ZFAILEDBANKDETAILS(Z_PK, Z_ENT, Z_OPT, ZINFO,
ZUPDATEDDATE, ZZIP, ZCLOSEDATE) VALUES (?, ?, ?, ?, ?, ?, ?) INSERT INTO ZFAILEDBANKINFO(Z_PK, Z_ENT, Z_OPT, ZDETAILS, ZNAME,
ZSTATE, ZCITY) VALUES (?, ?, ?, ?, ?, ?, ?) SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSTATE, t0.ZCITY, t0.ZDETAILS
FROM ZFAILEDBANKINFO t0 SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZUPDATEDDATE, t0.ZZIP, t0.ZCLOSEDATE,
t0.ZINFO FROM ZFAILEDBANKDETAILS t0 WHERE t0.Z_PK = ? |
然后有对details和info表操作的insert into语句。随后,我们在查询中select整个bank info表。然后遍历结果.每当访问details变量的时候,Core Data 报错并在另一个表ZFAILEDBANKDETAILS中执行select语句。
在此之前,我们已经使用了NSManagedObject来操作我们的实体(Entities)。这其实不是最好的办法,因为这样很容易写错特性(attribute)名或者搞错数据的类型等等。
更好的方式是为每个实体(entity)建立一个模型(Model )文件。你可以手工实现,但是用XCode类生成器会更简单。
让我们来试试看。打开FailedBanksCD.xcdatamodel,单击FailedBankInfo实体(entity),然后去到File\New File。选择“Cocoa Touch Class”,你会看到一个关于“Managed Object Class”的新模板。选择它并单击Next,再Next到下一视图。
在第三个视图中,XCode允许你选择实体来产生类。为了节约时间,确保FailedBankInfo和FailedBankDetails都被选中,然后单击Finish。
你可以看到一些新文件添加到了你的工程中。FailedBankInfo.h/m以及 FailedBankDetails.h/m。这些类非常简单,仅仅根据你刚刚添加的实体做出的属性(properties)声明。你会注意到这些属性(properties)在.m文件中声明为dynamic,这是因为Core Data会自动连接这些属性(properties)。
我有注意到自动产生类的一个问题并修正了它。如果你检查一下FailedBankDetails.h,你会发现info变量正确地声明为FailedBankInfo类,而FailedBankInfo.h 中的details变量则定义成了一个NSManagedObject(但本应是一个FailedBankDetails对象)。你可以通过在FailedBankDetails文件的顶端加入预声明来修正这一错误。
<
@class FailedBankDetails; |
之后如下改变details的属性(property)声明:
@property (nonatomic, retain) FailedBankDetails * details; |
同样,回头瞧下FailedBanksCD.xcdatamodel.当你查看实体的属性(property)时,你会发现它的类名已经自动设为你刚刚在自动生成类里面更改的内容了。
现在,让我们调整app委托的测试代码,用上新的NSManagedObject子类。首先导入头文件:
#import "FailedBankInfo.h" #import "FailedBankDetails.h" |
然后跟下面一样改变代码:
NSManagedObjectContext *context = [self managedObjectContext]; FailedBankInfo *failedBankInfo = [NSEntityDescription insertNewObjectForEntityForName:@"FailedBankInfo" inManagedObjectContext:context]; failedBankInfo.name = @"Test Bank"; failedBankInfo.city = @"Testville"; failedBankInfo.state = @"Testland"; FailedBankDetails *failedBankDetails = [NSEntityDescription insertNewObjectForEntityForName:@"FailedBankDetails" inManagedObjectContext:context]; failedBankDetails.closeDate = [NSDate date]; failedBankDetails.updatedDate = [NSDate date]; failedBankDetails.zip = [NSNumber numberWithInt:12345]; failedBankDetails.info = failedBankInfo; failedBankInfo.details = failedBankDetails; NSError *error; if (![context save:&error]) { NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]); } // Test listing all FailedBankInfos from the store NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"FailedBankInfo" inManagedObjectContext:context]; [fetchRequest setEntity:entity]; NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error]; for (FailedBankInfo *info in fetchedObjects) { NSLog(@"Name: %@", info.name); FailedBankDetails *details = info.details; NSLog(@"Zip: %@", details.zip); } [fetchRequest release]; |
这跟我们之前写的代码很像,除了用新的子类替代直接NSManagedObject的部分。现在我们安全又简洁的代码完成了!
右键Classes并点击“Add\New File…” 选择“UIViewController subclass”,确保“UITableVIewController subclass”被选中,“With XIB for user interface”不被选中。将类命名为FailedBanksListViewController。打开FailedBanksListViewController.h并添加2个成员变量:
完成后代码如下所示:
#import <UIKit/UIKit.h> @interface FailedBanksListViewController : UITableViewController { NSArray *_failedBankInfos; NSManagedObjectContext *_context; } @property (nonatomic, retain) NSArray *failedBankInfos; @property (nonatomic, retain) NSManagedObjectContext *context; @end |
跳转到FailedBanksListViewController.m并加入一些import,你的synthesize语句,以及清理代码。
// At very top, in import section #import "FailedBankInfo.h" // At top, under @implementation @synthesize failedBankInfos = _failedBankInfos; @synthesize context = _context; // In dealloc self.failedBankInfos = nil; self.context = nil; |
然后取消viewDidLoad的注释并改成下面的样子:
- (void)viewDidLoad { [super viewDidLoad]; NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"FailedBankInfo" inManagedObjectContext:_context]; [fetchRequest setEntity:entity]; NSError *error; self.failedBankInfos = [_context executeFetchRequest:fetchRequest error:&error]; self.title = @"Failed Banks"; [fetchRequest release]; } |
这段代码应该看起来很像早前的测试代码。我们简单的创建了一个获取请求来得到数据库中FailedBankInfos的全部内容,然后存储到我们的成员变量里面。
其余改动就跟我们在SQLite教程中所做的一样。为了快速参考,我将再次列出剩下的步骤。
对numberOfSectionsInTableView返回1:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } |
用以下代码替代numberOfRowsInSection:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease]; } // Set up the cell... FailedBankInfo *info = [_failedBankInfos objectAtIndex:indexPath.row]; cell.textLabel.text = info.name; cell.detailTextLabel.text = [NSString stringWithFormat:@"%@, %@", info.city, info.state]; return cell; } |
对我们刚刚加进来的UINavigationController ,在FailedBanksCDAppDelegate.h中增加一个outlet
@interface FailedBanksCDAppDelegate : NSObject <UIApplicationDelegate> { NSManagedObjectModel *managedObjectModel; NSManagedObjectContext *managedObjectContext; NSPersistentStoreCoordinator *persistentStoreCoordinator; UIWindow *window; UINavigationController *_navController; } @property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel; @property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext; @property (nonatomic, retain, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; @property (nonatomic, retain) IBOutlet UIWindow *window; @property (nonatomic, retain) IBOutlet UINavigationController *navController; - (NSString *)applicationDocumentsDirectory; @end |
打开Resources并双击MainWindow.xib。在库中拖出一个Navigation Controller。单击下箭头,选中View Controller,在特性(attribute)面板的第四个标签(tab)中将“Class”改成“FailedBanksListViewController”。
最后,按住control用鼠标(或者右键)从MainWindow.xib中的“FailedBanksCD App Delegate”拖到“Navigation Controller”,并连接其“navController”outlet。保存xlib并关闭。
现在我们只需要在FailedBanksCDAppDelegate.m中添加几行代码:
// At top #import "FailedBanksListViewController.h" // Under @implementation @synthesize navController = _navController; // In applicationDisFinishLaunching, before makeKeyAndVisible: FailedBanksListViewController *root = (FailedBanksListViewController *) [_navController topViewController]; root.context = [self managedObjectContext]; [window addSubview:_navController.view]; // In dealloc self.navController = nil; |
编译工程并运行,如果一切顺利的话你会看到下面的示例银行:
这里是迄今为止的示例代码
到目前为止一切尚称良好—除了failed banks的真实数据。所以系列的下一教程我们会涉及 如何预载/导入已有数据!
联系客服