在iOS開發中數據存儲的方式可以歸納為兩類:一類是存儲為文件,另一類是存儲到數據庫。例如前面IOS開發系列—Objective-C之Foundation框架的文章中提到歸檔、plist文件存儲,包括偏好設置其本質都是存儲為文件,只是說歸檔或者plist文件存儲可以選擇保存到沙盒中,而偏好設置系統已經規定只能保存到沙盒的Library/PReferences目錄。當然,文件存儲并不作為本文的重點內容。本文重點還是說數據庫存儲,做過數據庫開發的朋友應該知道,可以通過SQL直接訪問數據庫,也可以通過ORM進行對象關系映射訪問數據庫。這兩種方式恰恰對應iOS中SQLite和Core Data的內容,在此將重點進行分析:
SQLite是目前主流的嵌入式關系型數據庫,其最主要的特點就是輕量級、跨平臺,當前很多嵌入式操作系統都將其作為數據庫首選。雖然SQLite是一款輕型數據庫,但是其功能也絕不亞于很多大型關系數據庫。學習數據庫就要學習其相關的定義、操作、查詢語言,也就是大家日常說得SQL語句。和其他數據庫相比,SQLite中的SQL語法并沒有太大的差別,因此這里對于SQL語句的內容不會過多贅述,大家可以參考SQLite中其他SQL相關的內容,這里還是重點講解iOS中如何使用SQLite構建應用程序。先看一下SQLite數據庫的幾個特點:
要使用SQLite很簡單,如果在Mac OSX上使用可以考慮到SQLite官方網站下載命令行工具,也可以使用類似于SQLiteManager、Navicat for SQLite等工具。為了方便大家開發調試,建議在開發環境中安裝上述工具。
在iOS中操作SQLite數據庫可以分為以下幾步(注意先在項目中導入libsqlite3框架):
在整個操作過程中無需管理數據庫連接,對于嵌入式SQLite操作是持久連接(盡管可以通過sqlite3_close()關閉),不需要開發人員自己釋放連接。縱觀整個操作過程,其實與其他平臺的開發沒有明顯的區別,較為麻煩的就是數據讀取,在iOS平臺中使用C進行數據讀取采用了游標的形式,每次只能讀取一行數據,較為麻煩。因此實際開發中不妨對這些操作進行封裝:
KCDbManager.h
//// DbManager.h// Dataaccess//// Created by Kenshin Cui on 14-3-29.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import <Foundation/Foundation.h>#import <sqlite3.h>#import "KCSingleton.h"@interface KCDbManager : NSObjectsingleton_interface(KCDbManager);#pragma mark - 屬性#pragma mark 數據庫引用,使用它進行數據庫操作@property (nonatomic) sqlite3 *database;#pragma mark - 共有方法/** * 打開數據庫 * * @param dbname 數據庫名稱 */-(void)openDb:(NSString *)dbname;/** * 執行無返回值的sql * * @param sql sql語句 */-(void)executeNonQuery:(NSString *)sql;/** * 執行有返回值的sql * * @param sql sql語句 * * @return 查詢結果 */-(NSArray *)executeQuery:(NSString *)sql;@end
KCDbManager.m
//// DbManager.m// DataAccess//// Created by Kenshin Cui on 14-3-29.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import "KCDbManager.h"#import <sqlite3.h>#import "KCSingleton.h"#import "KCAppConfig.h"#ifndef kDatabaseName#define kDatabaseName @"myDatabase.db"#endif@interface KCDbManager()@end@implementation KCDbManagersingleton_implementation(KCDbManager)#pragma mark 重寫初始化方法-(instancetype)init{ KCDbManager *manager; if((manager=[super init])) { [manager openDb:kDatabaseName]; } return manager;}-(void)openDb:(NSString *)dbname{ //取得數據庫保存路徑,通常保存沙盒Documents目錄 NSString *directory=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; NSLog(@"%@",directory); NSString *filePath=[directory stringByAppendingPathComponent:dbname]; //如果有數據庫則直接打開,否則創建并打開(注意filePath是ObjC中的字符串,需要轉化為C語言字符串類型) if (SQLITE_OK ==sqlite3_open(filePath.UTF8String, &_database)) { NSLog(@"數據庫打開成功!"); }else{ NSLog(@"數據庫打開失敗!"); }}-(void)executeNonQuery:(NSString *)sql{ char *error; //單步執行sql語句,用于插入、修改、刪除 if (SQLITE_OK!=sqlite3_exec(_database, sql.UTF8String, NULL, NULL,&error)) { NSLog(@"執行SQL語句過程中發生錯誤!錯誤信息:%s",error); }}-(NSArray *)executeQuery:(NSString *)sql{ NSMutableArray *rows=[NSMutableArray array];//數據行 //評估語法正確性 sqlite3_stmt *stmt; //檢查語法正確性 if (SQLITE_OK==sqlite3_prepare_v2(_database, sql.UTF8String, -1, &stmt, NULL)) { //單步執行sql語句 while (SQLITE_ROW==sqlite3_step(stmt)) { int columnCount= sqlite3_column_count(stmt); NSMutableDictionary *dic=[NSMutableDictionary dictionary]; for (int i=0; i<columnCount; i++) { const char *name= sqlite3_column_name(stmt, i);//取得列名 const unsigned char *value= sqlite3_column_text(stmt, i);//取得某列的值 dic[[NSString stringWithUTF8String:name]]=[NSString stringWithUTF8String:(const char *)value]; } [rows addObject:dic]; } } //釋放句柄 sqlite3_finalize(stmt); return rows;}@end
在上面的類中對于數據庫操作進行了封裝,封裝之后數據操作更加方便,同時所有的語法都由C轉換成了ObjC。
下面仍然以微博查看為例進行SQLite演示。當然實際開發中微博數據是從網絡讀取的,但是考慮到緩存問題,通常會選擇將微博數據保存到本地,下面的Demo演示了將數據存放到本地數據庫以及數據讀取的過程。當然,實際開發中并不會在視圖控制器中直接調用數據庫操作方法,在這里通常會引入兩個概念Model和Service。Model自不必多說,就是MVC中的模型。而Service指的是操作數據庫的服務層,它封裝了對于Model的基本操作方法,實現具體的業務邏輯。為了解耦,在控制器中是不會直接接觸數據庫的,控制器中只和模型(模型是領域的抽象)、服務對象有關系,借助服務層對模型進行各類操作,模型的操作反應到數據庫中就是對表中數據的操作。具體關系如下:
要完成上述功能,首先定義一個應用程序全局對象進行數據庫、表的創建。為了避免每次都創建數據庫和表出錯,這里利用了偏好設置進行保存當前創建狀態(其實這也是數據存儲的一部分),如果創建過了數據庫則不再創建,否則創建數據庫和表。
KCDatabaseCreator.m
//// KCDatabaseCreator.m// DataAccess//// Created by Kenshin Cui on 14-3-29.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import "KCDatabaseCreator.h"#import "KCDbManager.h"@implementation KCDatabaseCreator+(void)initDatabase{ NSString *key=@"IsCreatedDb"; NSUserDefaults *defaults=[[NSUserDefaults alloc]init]; if ([[defaults valueForKey:key] intValue]!=1) { [self createUserTable]; [self createStatusTable]; [defaults setValue:@1 forKey:key]; }}+(void)createUserTable{ NSString *sql=@"CREATE TABLE User (Id integer PRIMARY KEY AUTOINCREMENT,name text,screenName text, profileImageUrl text,mbtype text,city text)"; [[KCDbManager sharedKCDbManager] executeNonQuery:sql];}+(void)createStatusTable{ NSString *sql=@"CREATE TABLE Status (Id integer PRIMARY KEY AUTOINCREMENT,source text,createdAt date,/"text/" text,user integer REFERENCES User (Id))"; [[KCDbManager sharedKCDbManager] executeNonQuery:sql];}@end
其次,定義數據模型,這里定義用戶User和微博Status兩個數據模型類。注意模型應該盡量保持其單純性,僅僅是簡單的POCO,不要引入視圖、控制器等相關內容。
KCUser.h
//// KCUser.h// UrlConnection//// Created by Kenshin Cui on 14-3-22.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import <Foundation/Foundation.h>@interface KCUser : NSObject#pragma mark 編號@property (nonatomic,strong) NSNumber *Id;#pragma mark 用戶名@property (nonatomic,copy) NSString *name;#pragma mark 用戶昵稱@property (nonatomic,copy) NSString *screenName;#pragma mark 頭像@property (nonatomic,copy) NSString *profileImageUrl;#pragma mark 會員類型@property (nonatomic,copy) NSString *mbtype;#pragma mark 城市@property (nonatomic,copy) NSString *city;#pragma mark - 動態方法/** * 初始化用戶 * * @param name 用戶名 * @param city 所在城市 * * @return 用戶對象 */-(KCUser *)initWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city;/** * 使用字典初始化用戶對象 * * @param dic 用戶數據 * * @return 用戶對象 */-(KCUser *)initWithDictionary:(NSDictionary *)dic;#pragma mark - 靜態方法+(KCUser *)userWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city;@end
KCUser.m
//// KCUser.m// UrlConnection//// Created by Kenshin Cui on 14-3-22.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import "KCUser.h"@implementation KCUser-(KCUser *)initWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city{ if (self=[super init]) { self.name=name; self.screenName=screenName; self.profileImageUrl=profileImageUrl; self.mbtype=mbtype; self.city=city; } return self;}-(KCUser *)initWithDictionary:(NSDictionary *)dic{ if (self=[super init]) { [self setValuesForKeysWithDictionary:dic]; } return self;}+(KCUser *)userWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city{ KCUser *user=[[KCUser alloc]initWithName:name screenName:screenName profileImageUrl:profileImageUrl mbtype:mbtype city:city]; return user;}@end
KCStatus.h
//// KCStatus.h// UITableView//// Created by Kenshin Cui on 14-3-1.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import <Foundation/Foundation.h>#import "KCUser.h"@interface KCStatus : NSObject#pragma mark - 屬性@property (nonatomic,strong) NSNumber *Id;//微博id@property (nonatomic,strong) KCUser *user;//發送用戶@property (nonatomic,copy) NSString *createdAt;//創建時間@property (nonatomic,copy) NSString *source;//設備來源@property (nonatomic,copy) NSString *text;//微博內容#pragma mark - 動態方法/** * 初始化微博數據 * * @param createAt 創建日期 * @param source 來源 * @param text 微博內容 * @param user 發送用戶 * * @return 微博對象 */-(KCStatus *)initWithCreateAt:(NSString *)createAt source:(NSString *)source text:(NSString *)text user:(KCUser *)user;/** * 初始化微博數據 * * @param profileImageUrl 用戶頭像 * @param mbtype 會員類型 * @param createAt 創建日期 * @param source 來源 * @param text 微博內容 * @param userId 用戶編號 * * @return 微博對象 */-(KCStatus *)initWithCreateAt:(NSString *)createAt source:(NSString *)source text:(NSString *)text userId:(int)userId;/** * 使用字典初始化微博對象 * * @param dic 字典數據 * * @return 微博對象 */-(KCStatus *)initWithDictionary:(NSDictionary *)dic;#pragma mark - 靜態方法/** * 初始化微博數據 * * @param createAt 創建日期 * @param source 來源 * @param text 微博內容 * @param user 發送用戶 * * @return 微博對象 */+(KCStatus *)statusWithCreateAt:(NSString *)createAt source:(NSString *)source text:(NSString *)text user:(KCUser *)user;/** * 初始化微博數據 * * @param profileImageUrl 用戶頭像 * @param mbtype 會員類型 * @param createAt 創建日期 * @param source 來源 * @param text 微博內容 * @param userId 用戶編號 * * @return 微博對象 */+(KCStatus *)statusWithCreateAt:(NSString *)createAt source:(NSString *)source text:(NSString *)text userId:(int)userId;@end
KCStatus.m
//// KCStatus.m// UITableView//// Created by Kenshin Cui on 14-3-1.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import "KCStatus.h"@implementation KCStatus-(KCStatus *)initWithDictionary:(NSDictionary *)dic{ if (self=[super init]) { [self setValuesForKeysWithDictionary:dic]; self.user=[[KCUser alloc]init]; self.user.Id=dic[@"user"]; } return self;}-(KCStatus *)initWithCreateAt:(NSString *)createAt source:(NSString *)source text:(NSString *)text user:(KCUser *)user{ if (self=[super init]) { self.createdAt=createAt; self.source=source; self.text=text; self.user=user; } return self;}-(KCStatus *)initWithCreateAt:(NSString *)createAt source:(NSString *)source text:(NSString *)text userId:(int)userId{ if (self=[super init]) { self.createdAt=createAt; self.source=source; self.text=text; KCUser *user=[[KCUser alloc]init]; user.Id=[NSNumber numberWithInt:userId]; self.user=user; } return self;}-(NSString *)source{ return [NSString stringWithFormat:@"來自 %@",_source];}+(KCStatus *)statusWithCreateAt:(NSString *)createAt source:(NSString *)source text:(NSString *)text user:(KCUser *)user{ KCStatus *status=[[KCStatus alloc]initWithCreateAt:createAt source:source text:text user:user]; return status;}+(KCStatus *)statusWithCreateAt:(NSString *)createAt source:(NSString *)source text:(NSString *)text userId:(int)userId{ KCStatus *status=[[KCStatus alloc]initWithCreateAt:createAt source:source text:text userId:userId]; return status;}@end
然后,編寫服務類,進行數據的增、刪、改、查操作,由于服務類方法同樣不需要過多的配置,因此定義為單例,保證程序中只有一個實例即可。服務類中調用前面封裝的數據庫方法將對數據庫的操作轉換為對模型的操作。
KCUserService.h
//// KCUserService.h// DataAccess//// Created by Kenshin Cui on 14-3-29.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import <Foundation/Foundation.h>#import "KCUser.h"#import "KCSingleton.h"@interface KCUserService : NSObjectsingleton_interface(KCUserService)/** * 添加用戶信息 * * @param user 用戶對象 */-(void)addUser:(KCUser *)user;/** * 刪除用戶 * * @param user 用戶對象 */-(void)removeUser:(KCUser *)user;/** * 根據用戶名刪除用戶 * * @param name 用戶名 */-(void)removeUserByName:(NSString *)name;/** * 修改用戶內容 * * @param user 用戶對象 */-(void)modifyUser:(KCUser *)user;/** * 根據用戶編號取得用戶 * * @param Id 用戶編號 * * @return 用戶對象 */-(KCUser *)getUserById:(int)Id;/** * 根據用戶名取得用戶 * * @param name 用戶名 * * @return 用戶對象 */-(KCUser *)getUserByName:(NSString *)name;@end
KCUserService.m
//// KCUserService.m// DataAccess//// Created by Kenshin Cui on 14-3-29.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import "KCUserService.h"#import "KCUser.h"#import "KCDbManager.h"@implementation KCUserServicesingleton_implementation(KCUserService)-(void)addUser:(KCUser *)user{ NSString *sql=[NSString stringWithFormat:@"INSERT INTO User (name,screenName, profileImageUrl,mbtype,city) VALUES('%@','%@','%@','%@','%@')",user.name,user.screenName, user.profileImageUrl,user.mbtype,user.city]; [[KCDbManager sharedKCDbManager] executeNonQuery:sql];}-(void)removeUser:(KCUser *)user{ NSString *sql=[NSString stringWithFormat:@"DELETE FROM User WHERE Id='%@'",user.Id]; [[KCDbManager sharedKCDbManager] executeNonQuery:sql];}-(void)removeUserByName:(NSString *)name{ NSString *sql=[NSString stringWithFormat:@"DELETE FROM User WHERE name='%@'",name]; [[KCDbManager sharedKCDbManager] executeNonQuery:sql];}-(void)modifyUser:(KCUser *)user{ NSString *sql=[NSString stringWithFormat:@"UPDATE User SET name='%@',screenName='%@',profileImageUrl='%@',mbtype='%@',city='%@' WHERE Id='%@'",user.name,user.screenName,user.profileImageUrl,user.mbtype,user.city,user.Id]; [[KCDbManager sharedKCDbManager] executeNonQuery:sql];}-(KCUser *)getUserById:(int)Id{ KCUser *user=[[KCUser alloc]init]; NSString *sql=[NSString stringWithFormat:@"SELECT name,screenName,profileImageUrl,mbtype,city FROM User WHERE Id='%i'", Id]; NSArray *rows= [[KCDbManager sharedKCDbManager] executeQuery:sql]; if (rows&&rows.count>0) { [user setValuesForKeysWithDictionary:rows[0]]; } return user;}-(KCUser *)getUserByName:(NSString *)name{ KCUser *user=[[KCUser alloc]init]; NSString *sql=[NSString stringWithFormat:@"SELECT Id, name,screenName,profileImageUrl,mbtype,city FROM User WHERE name='%@'", name]; NSArray *rows= [[KCDbManager sharedKCDbManager] executeQuery:sql]; if (rows&&rows.count>0) { [user setValuesForKeysWithDictionary:rows[0]]; } return user;}@end
KCStatusService.h
//// KCStatusService.h// DataAccess//// Created by Kenshin Cui on 14-3-29.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import <Foundation/Foundation.h>#import "KCSingleton.h"@class KCStatus;@interface KCStatusService : NSObjectsingleton_interface(KCStatusService)/** * 添加微博信息 * * @param status 微博對象 */-(void)addStatus:(KCStatus *)status;/** * 刪除微博 * * @param status 微博對象 */-(void)removeStatus:(KCStatus *)status;/** * 修改微博內容 * * @param status 微博對象 */-(void)modifyStatus:(KCStatus *)status;/** * 根據編號取得微博 * * @param Id 微博編號 * * @return 微博對象 */-(KCStatus *)getStatusById:(int)Id;/** * 取得所有微博對象 * * @return 所有微博對象 */-(NSArray *)getAllStatus;@end
KCStatusService.m
//// KCStatusService.m// DataAccess//// Created by Kenshin Cui on 14-3-29.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import "KCStatusService.h"#import "KCDbManager.h"#import "KCStatus.h"#import "KCUserService.h"#import "KCSingleton.h"@interface KCStatusService(){ }@end@implementation KCStatusServicesingleton_implementation(KCStatusService)-(void)addStatus:(KCStatus *)status{ NSString *sql=[NSString stringWithFormat:@"INSERT INTO Status (source,createdAt,/"text/" ,user) VALUES('%@','%@','%@','%@')",status.source,status.createdAt,status.text,status.user.Id]; [[KCDbManager sharedKCDbManager] executeNonQuery:sql];}-(void)removeStatus:(KCStatus *)status{ NSString *sql=[NSString stringWithFormat:@"DELETE FROM Status WHERE Id='%@'",status.Id]; [[KCDbManager sharedKCDbManager] executeNonQuery:sql];}-(void)modifyStatus:(KCStatus *)status{ NSString *sql=[NSString stringWithFormat:@"UPDATE Status SET source='%@',createdAt='%@',/"text/"='%@' ,user='%@' WHERE Id='%@'",status.source,status.createdAt,status.text,status.user, status.Id]; [[KCDbManager sharedKCDbManager] executeNonQuery:sql];}-(KCStatus *)getStatusById:(int)Id{ KCStatus *status=[[KCStatus alloc]init]; NSString *sql=[NSString stringWithFormat:@"SELECT Id, source,createdAt,/"text/" ,user FROM Status WHERE Id='%i'", Id]; NSArray *rows= [[KCDbManager sharedKCDbManager] executeQuery:sql]; if (rows&&rows.count>0) { [status setValuesForKeysWithDictionary:rows[0]]; status.user=[[KCUserService sharedKCUserService] getUserById:[(NSNumber *)rows[0][@"user"] intValue]] ; } return status;}-(NSArray *)getAllStatus{ NSMutableArray *array=[NSMutableArray array]; NSString *sql=@"SELECT Id, source,createdAt,/"text/" ,user FROM Status ORDER BY Id"; NSArray *rows= [[KCDbManager sharedKCDbManager] executeQuery:sql]; for (NSDictionary *dic in rows) { KCStatus *status=[self getStatusById:[(NSNumber *)dic[@"Id"] intValue]]; [array addObject:status]; } return array;}@end
最后,在視圖控制器中調用相應的服務層進行各類數據操作,在下面的代碼中分別演示了增、刪、改、查四類操作。
KCMainViewController.m
//// KCMainTableViewController.m// DataAccess//// Created by Kenshin Cui on 14-3-29.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import "KCMainTableViewController.h"#import "KCDbManager.h"#import "KCDatabaseCreator.h"#import "KCUser.h"#import "KCStatus.h"#import "KCUserService.h"#import "KCStatusService.h"#import "KCStatusTableViewCell.h"@interface KCMainTableViewController (){ NSArray *_status; NSMutableArray *_statusCells;}@end@implementation KCMainTableViewController- (void)viewDidLoad { [super viewDidLoad]; [KCDatabaseCreator initDatabase]; // [self addUsers];// [self removeUser];// [self modifyUserInfo]; // [self addStatus]; [self loadStatusData]; }-(void)addUsers{ KCUser *user1=[KCUser userWithName:@"Binger" screenName:@"冰兒" profileImageUrl:@"binger.jpg" mbtype:@"mbtype.png" city:@"北京"]; [[KCUserService sharedKCUserService] addUser:user1]; KCUser *user2=[KCUser userWithName:@"Xiaona" screenName:@"小娜" profileImageUrl:@"xiaona.jpg" mbtype:@"mbtype.png" city:@"北京"]; [[KCUserService sharedKCUserService] addUser:user2]; KCUser *user3=[KCUser userWithName:@"Lily" screenName:@"麗麗" profileImageUrl:@"lily.jpg" mbtype:@"mbtype.png" city:@"北京"]; [[KCUserService sharedKCUserService] addUser:user3]; KCUser *user4=[KCUser userWithName:@"Qianmo" screenName:@"阡陌" profileImageUrl:@"qianmo.jpg" mbtype:@"mbtype.png" city:@"北京"]; [[KCUserService sharedKCUserService] addUser:user4]; KCUser *user5=[KCUser userWithName:@"Yanyue" screenName:@"炎月" profileImageUrl:@"yanyue.jpg" mbtype:@"mbtype.png" city:@"北京"]; [[KCUserService sharedKCUserService] addUser:user5];}-(void)addStatus{ KCStatus *status1=[KCStatus statusWithCreateAt:@"9:00" source:@"iphone 6" text:@"一只雪猴在日本邊泡溫泉邊玩iPhone的照片,獲得了/"2014年野生動物攝影師/"大賽特等獎。一起來為猴子配個詞" userId:1]; [[KCStatusService sharedKCStatusService] addStatus:status1]; KCStatus *status2=[KCStatus statusWithCreateAt:@"9:00" source:@"iPhone 6" text:@"一只雪猴在日本邊泡溫泉邊玩iPhone的照片,獲得了/"2014年野生動物攝影師/"大賽特等獎。一起來為猴子配個詞" userId:1]; [[KCStatusService sharedKCStatusService] addStatus:status2]; KCStatus *status3=[KCStatus statusWithCreateAt:@"9:30" source:@"iPhone 6" text:@"【我們送iPhone6了 要求很簡單】真心回饋粉絲,小編覺得現在最好的獎品就是iPhone6了。今起到12月31日,關注我們,轉發微博,就有機會獲iPhone6(獎品可能需要等待)!每月抽一臺[鼓掌]。不費事,還是試試吧,萬一中了呢" userId:2]; [[KCStatusService sharedKCStatusService] addStatus:status3]; KCStatus *status4=[KCStatus statusWithCreateAt:@"9:45" source:@"iPhone 6" text:@"重大新聞:蒂姆庫克宣布出柜后,ISIS戰士怒扔iPhone,沙特神職人員呼吁人們換回iPhone 4。[via Pan-Arabia Enquirer]" userId:3]; [[KCStatusService sharedKCStatusService] addStatus:status4]; KCStatus *status5=[KCStatus statusWithCreateAt:@"10:05" source:@"iPhone 6" text:@"小伙伴們,有誰知道怎么往Iphone4S里倒東西?倒入的東西又該在哪里找?用了Iphone這么長時間,還真的不知道怎么弄!有誰知道???謝謝!" userId:4]; [[KCStatusService sharedKCStatusService] addStatus:status5]; KCStatus *status6=[KCStatus statusWithCreateAt:@"10:07" source:@"iPhone 6" text:@"在音悅臺iPhone客戶端里發現一個悅單《Infinite 金明洙》,推薦給大家! " userId:1]; [[KCStatusService sharedKCStatusService] addStatus:status6]; KCStatus *status7=[KCStatus statusWithCreateAt:@"11:20" source:@"iPhone 6" text:@"如果sony吧mp3播放器產品發展下去,不貪圖手頭節目源的現實利益,就木有蘋果的ipod,也就木有iphone??逻_類似的現實利益,不自我革命的案例也是一種巨頭的宿命。" userId:2]; [[KCStatusService sharedKCStatusService] addStatus:status7]; KCStatus *status8=[KCStatus statusWithCreateAt:@"13:00" source:@"iPhone 6" text:@"【iPhone 7 Plus】新買的iPhone 7 Plus ,如何?夠酷炫么?" userId:2]; [[KCStatusService sharedKCStatusService] addStatus:status8]; KCStatus *status9=[KCStatus statusWithCreateAt:@"13:24" source:@"iPhone 6" text:@"自拍神器#卡西歐TR500#,tr350S~價格美麗,行貨,全國聯保~iPhone6 iPhone6Plus卡西歐TR150 TR200 TR350 TR350S全面到貨 招收各種代理![給力]微信:39017366" userId:3]; [[KCStatusService sharedKCStatusService] addStatus:status9]; KCStatus *status10=[KCStatus statusWithCreateAt:@"13:26" source:@"iPhone 6" text:@"猜到猴哥玩手機時所思所想者,再獎iPhone一部。(獎品由“2014年野生動物攝影師”評委會頒發)" userId:3]; [[KCStatusService sharedKCStatusService] addStatus:status10];}-(void)removeUser{ //注意在SQLite中區分大小寫 [[KCUserService sharedKCUserService] removeUserByName:@"Yanyue"];}-(void)modifyUserInfo{ KCUser *user1= [[KCUserService sharedKCUserService]getUserByName:@"Xiaona"]; user1.city=@"上海"; [[KCUserService sharedKCUserService] modifyUser:user1]; KCUser *user2= [[KCUserService sharedKCUserService]getUserByName:@"Lily"]; user2.city=@"深圳"; [[KCUserService sharedKCUserService] modifyUser:user2];}#pragma mark 加載數據-(void)loadStatusData{ _statusCells=[[NSMutableArray alloc]init]; _status=[[KCStatusService sharedKCStatusService]getAllStatus]; [_status enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { KCStatusTableViewCell *cell=[[KCStatusTableViewCell alloc]init]; cell.status=(KCStatus *)obj; [_statusCells addObject:cell]; }]; NSLog(@"%@",[_status lastObject]);}#pragma mark - Table view data source- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1;}- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return _status.count;}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *identtityKey=@"myTableViewCellIdentityKey1"; KCStatusTableViewCell *cell=[self.tableView dequeueReusableCellWithIdentifier:identtityKey]; if(cell==nil){ cell=[[KCStatusTableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identtityKey]; } cell.status=_status[indexPath.row]; return cell;}-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ return ((KCStatusTableViewCell *)_statusCells[indexPath.row]).height;}-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{ return 20.0f;}@end
項目目錄結構:
運行效果:
當前,各類應用開發中只要牽扯到數據庫操作通常都會用到一個概念“對象關系映射(ORM)”。例如在java平臺使用Hibernate,在.NET平臺使用Entity Framework、Linq、NHibernate等。在iOS中也不例外,iOS中ORM框架首選Core Data,這是官方推薦的,不需要借助第三方框架。無論是哪種平臺、哪種技術,ORM框架的作用都是相同的,那就是將關系數據庫中的表(準確的說是實體)轉換為程序中的對象,其本質還是對數據庫的操作(例如Core Data中如果存儲類型配置為SQLite則本質還是操作的SQLite數據庫)。細心的朋友應該已經注意到,在上面的SQLite中其實我們在KCMainViewController中進行的數據庫操作已經轉換為了對象操作,服務層中的方法中已經將對數據庫的操作封裝起來,轉換為了對Model的操作,這種方式已經是面向對象的。上述通過將對象映射到實體的過程完全是手動完成的,相對來說操作比較復雜,就拿對KCStatus對象的操作來說:首先要手動創建數據庫(Status表),其次手動創建模型KCStatus,接著創建服務層KCStatusService。Core Data正是為了解決這個問題而產生的,它將數據庫的創建、表的創建、對象和表的轉換等操作封裝起來,簡化了我們的操作(注意Core Data只是將對象關系的映射簡化了,并不是把服務層替代了,這一點大家需要明白)。
使用Core Data進行數據庫存取并不需要手動創建數據庫,這個過程完全由Core Data框架完成,開發人員面對的是模型,主要的工作就是把模型創建起來,具體數據庫如何創建則不用管。在iOS項目中添加“Data Model”文件。然后在其中創建實體和關系:
模型創建的過程中需要注意:
以上模型創建后,接下來就是根據上面的模型文件(.xcdatamodeld文件)生成具體的實體類。在Xcode中添加“NSManagedObject Subclass”文件,按照步驟選擇創建的模型及實體,Xcode就會根據所創建模型生成具體的實體類。
User.h
//// User.h// CoreData//// Created by Kenshin Cui on 14/03/27.// Copyright (c) 2014年 cmjstudio. All rights reserved.//#import <Foundation/Foundation.h>#import <CoreData/CoreData.h>@class Status;@interface User : NSManagedObject@property (nonatomic, retain) NSString * city;@property (nonatomic, retain) NSString * mbtype;@property (nonatomic, retain) NSString * name;@property (nonatomic, retain) NSString * profileImageUrl;@property (nonatomic, retain) NSString * screenName;@property (nonatomic, retain) NSSet *statuses;@end@interface User (CoreDataGeneratedAccessors)- (void)addStatusesObject:(Status *)value;- (void)removeStatusesObject:(Status *)value;- (void)addStatuses:(NSSet *)values;- (void)removeStatuses:(NSSet *)values;@end
User.m
//// User.m// CoreData//// Created by Kenshin Cui on 14/03/27.// Copyright (c) 2014年 cmjstudio. All rights reserved.//#import "User.h"#import "Status.h"@implementation User@dynamic city;@dynamic mbtype;@dynamic name;@dynamic profileImageUrl;@dynamic screenName;@dynamic statuses;@end
Status.h
//// Status.h// CoreData//// Created by Kenshin Cui on 14/03/27.// Copyright (c) 2014年 cmjstudio. All rights reserved.//#import <Foundation/Foundation.h>#import <CoreData/CoreData.h>@interface Status : NSManagedObject@property (nonatomic, retain) NSDate * createdAt;@property (nonatomic, retain) NSString * source;@property (nonatomic, retain) NSString * text;@property (nonatomic, retain) NSManagedObject *user;@end
Status.m
//// Status.m// CoreData//// Created by Kenshin Cui on 14/03/27.// Copyright (c) 2014年 cmjstudio. All rights reserved.//#import "Status.h"@implementation Status@dynamic createdAt;@dynamic source;@dynamic text;@dynamic user;@end
很顯然,通過模型生成類的過程相當簡單,通常這些類也不需要手動維護,如果模型發生的變化只要重新生成即可。有幾點需要注意:
當然,了解了這些還不足以完成數據的操作。究竟Core Data具體的設計如何,要完成數據的存取我們還需要了解一下Core Data幾個核心的類。
Core Data使用起來相對直接使用SQLite3的API而言更加的面向對象,操作過程通常分為以下幾個步驟:
1.創建管理上下文
創建管理上下可以細分為:加載模型文件->指定數據存儲路徑->創建對應數據類型的存儲->創建管理對象上下方并指定存儲。
經過這幾個步驟之后可以得到管理對象上下文NSManagedObjectContext,以后所有的數據操作都由此對象負責。同時如果是第一次創建上下文,Core Data會自動創建存儲文件(例如這里使用SQLite3存儲),并且根據模型對象創建對應的表結構。下圖為第一次運行生成的數據庫及相關映射文件:
為了方便后面使用,NSManagedObjectContext對象可以作為單例或靜態屬性來保存,下面是創建的管理對象上下文的主要代碼:
-(NSManagedObjectContext *)createDbContext{ NSManagedObjectContext *context; //打開模型文件,參數為nil則打開包中所有模型文件并合并成一個 NSManagedObjectModel *model=[NSManagedObjectModel mergedModelFromBundles:nil]; //創建解析器 NSPersistentStoreCoordinator *storeCoordinator=[[NSPersistentStoreCoordinator alloc]initWithManagedObjectModel:model]; //創建數據庫保存路徑 NSString *dir=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; NSLog(@"%@",dir); NSString *path=[dir stringByAppendingPathComponent:@"myDatabase.db"]; NSURL *url=[NSURL fileURLWithPath:path]; //添加SQLite持久存儲到解析器 NSError *error; [storeCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:nil error:&error]; if(error){ NSLog(@"數據庫打開失??!錯誤:%@",error.localizedDescription); }else{ context=[[NSManagedObjectContext alloc]init]; context.persistentStoreCoordinator=storeCoordinator; NSLog(@"數據庫打開成功!"); } return context;}
2.查詢數據
對于有條件的查詢,在Core Data中是通過謂詞來實現的。首先創建一個請求,然后設置請求條件,最后調用上下文執行請求的方法。
-(void)addUserWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city{ //添加一個對象 User *us= [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:self.context]; us.name=name; us.screenName=screenName; us.profileImageUrl=profileImageUrl; us.mbtype=mbtype; us.city=city; NSError *error; //保存上下文 if (![self.context save:&error]) { NSLog(@"添加過程中發生錯誤,錯誤信息:%@!",error.localizedDescription); }}
如果有多個條件,只要使用謂詞組合即可,那么對于關聯對象條件怎么查詢呢?這里分為兩種情況進行介紹:
a.查找一個對象只有唯一一個關聯對象的情況,例如查找用戶名為“Binger”的微博(一個微博只能屬于一個用戶),通過keypath查詢
-(NSArray *)getStatusesByUserName:(NSString *)name{ NSFetchRequest *request=[NSFetchRequest fetchRequestWithEntityName:@"Status"]; request.predicate=[NSPredicate predicateWithFormat:@"user.name=%@",name]; NSArray *array=[self.context executeFetchRequest:request error:nil]; return array;}
此時如果跟蹤Core Data生成的SQL語句會發現其實就是把Status表和User表進行了關聯查詢(JOIN連接)。
b.查找一個對象有多個關聯對象的情況,例如查找發送微博內容中包含“Watch”并且用戶昵稱為“小娜”的用戶(一個用戶有多條微博),此時可以充分利用謂詞進行過濾。
-(NSArray *)getUsersByStatusText:(NSString *)text screenName:(NSString *)screenName{ NSFetchRequest *request=[NSFetchRequest fetchRequestWithEntityName:@"Status"]; request.predicate=[NSPredicate predicateWithFormat:@"text LIKE '*Watch*'",text]; NSArray *statuses=[self.context executeFetchRequest:request error:nil]; NSPredicate *userPredicate= [NSPredicate predicateWithFormat:@"user.screenName=%@",screenName]; NSArray *users= [statuses filteredArrayUsingPredicate:userPredicate]; return users;}
注意如果單純查找微博中包含“Watch”的用戶,直接查出對應的微博,然后通過每個微博的user屬性即可獲得用戶,此時就不用使用額外的謂詞過濾條件。
3.插入數據
插入數據需要調用實體描述對象NSEntityDescription返回一個實體對象,然后設置對象屬性,最后保存當前上下文即可。這里需要注意,增、刪、改操作完最后必須調用管理對象上下文的保存方法,否則操作不會執行。
-(void)addUserWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city{ //添加一個對象 User *us= [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:self.context]; us.name=name; us.screenName=screenName; us.profileImageUrl=profileImageUrl; us.mbtype=mbtype; us.city=city; NSError *error; //保存上下文 if (![self.context save:&error]) { NSLog(@"添加過程中發生錯誤,錯誤信息:%@!",error.localizedDescription); }}
4.刪除數據
刪除數據可以直接調用管理對象上下文的deleteObject方法,刪除完保存上下文即可。注意,刪除數據前必須先查詢到對應對象。
-(void)removeUser:(User *)user{ [self.context deleteObject:user]; NSError *error; if (![self.context save:&error]) { NSLog(@"刪除過程中發生錯誤,錯誤信息:%@!",error.localizedDescription); }}
5.修改數據
修改數據首先也是取出對應的實體對象,然后通過修改對象的屬性,最后保存上下文。
-(void)modifyUserWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city{ User *us=[self getUserByName:name]; us.screenName=screenName; us.profileImageUrl=profileImageUrl; us.mbtype=mbtype; us.city=city; NSError *error; if (![self.context save:&error]) { NSLog(@"修改過程中發生錯誤,錯誤信息:%@",error.localizedDescription); }}
雖然Core Data(如果使用SQLite數據庫)操作最終轉換為SQL操作,但是調試起來卻不像操作SQL那么方便。特別是對于初學者而言經常出現查詢報錯的問題,如果能看到最終生成的SQL語句自然對于調試很有幫助。事實上在Xcode中是支持Core Data調試的,具體操作:Product-Scheme-Edit Scheme-Run-Arguments中依次添加兩個參數(注意參數順序不能錯):-com.apple.CoreData.SQLDebug、1。然后在運行程序過程中如果操作了數據庫就會將SQL語句打印在輸出面板。
注意:如果模型發生了變化,此時可以重新生成實體類文件,但是所生成的數據庫并不會自動更新,這時需要考慮重新生成數據庫并遷移原有的數據。
相比于SQLite3來說Core Data存在著諸多優勢,它面向對象,開發人員不必過多的關心更多數據庫操作知識,同時它基于ObjC操作,書寫更加優雅等。但是它本身也存在著一定的限制,例如如果考慮到跨平臺,則只能選擇SQLite,因為無論是iOS還是Android都可以使用同一個數據庫,降低了開發成本和維護成本。其次是當前多數ORM框架都存在的性能問題,因為ORM最終轉化為SQL操作,其中牽扯到模型數據轉化,其性能自然比不上直接使用SQL操作數據庫。那么有沒有更好的選擇呢?答案就是對SQLite進行封裝。
其實通過前面對于SQLite的分析,大家應該已經看到KCDbManager就是對于SQLite封裝的結果,開發人員面對的只有SQL和ObjC方法,不用過多libsqlite3的C語言API。但它畢竟只是一個簡單的封裝,還有更多的細節沒有考慮,例如如何處理并發安全性,如何更好的處理事務等。因此,這里推薦使用第三方框架FMDB,整個框架非常輕量級但又不失靈活性,也是很多企業開發的首選。
1.FMDB既然是對于libsqlite3框架的封裝,自然使用起來也是類似的,使用前也要打開一個數據庫,這個數據庫文件存在則直接打開否則會創建并打開。這里FMDB引入了一個FMDatabase對象來表示數據庫,打開數據庫和后面的數據庫操作全部依賴此對象。下面是打開數據庫獲得FMDatabase對象的代碼:
-(void)openDb:(NSString *)dbname{ //取得數據庫保存路徑,通常保存沙盒Documents目錄 NSString *directory=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; NSLog(@"%@",directory); NSString *filePath=[directory stringByAppendingPathComponent:dbname]; //創建FMDatabase對象 self.database=[FMDatabase databaseWithPath:filePath]; //打開數據上 if ([self.database open]) { NSLog(@"數據庫打開成功!"); }else{ NSLog(@"數據庫打開失敗!"); }}
注意:dataWithPath中的路徑參數一般會選擇保存到沙箱中的Documents目錄中;如果這個參數設置為nil則數據庫會在內存中創建;如果設置為@””則會在沙箱中的臨時目錄創建,應用程序關閉則文件刪除。
2.對于數據庫的操作跟前面KCDbManager的封裝是類似的,在FMDB中FMDatabase類提供了兩個方法executeUpdate:和executeQuery:分別用于執行無返回結果的查詢和有返回結果的查詢。當然這兩個方法有很多的重載這里就不詳細解釋了。唯一需要指出的是,如果調用有格式化參數的sql語句時,格式化符號使用“?”而不是“%@”、等。下面是兩種情況的代碼片段:
a.無返回結果
-(void)executeNonQuery:(NSString *)sql{ //執行更新sql語句,用于插入、修改、刪除 if (![self.database executeUpdate:sql]) { NSLog(@"執行SQL語句過程中發生錯誤!"); }}
b.有返回結果
-(NSArray *)executeQuery:(NSString *)sql{ NSMutableArray *array=[NSMutableArray array]; //執行查詢sql語句 FMResultSet *result= [self.database executeQuery:sql]; while (result.next) { NSMutableDictionary *dic=[NSMutableDictionary dictionary]; for (int i=0; i<result.columnCount; ++i) { dic[[result columnNameForIndex:i]]=[result stringForColumnIndex:i]; } [array addObject:dic]; } return array;}
對于有返回結果的查詢而言,查詢完返回一個游標FMResultSet,通過遍歷游標進行查詢。而且FMDB中提供了大量intForColumn、stringForColumn等方法進行取值。
我們知道直接使用libsqlite3進行數據庫操作其實是線程不安全的,如果遇到多個線程同時操作一個表的時候可能會發生意想不到的結果。為了解決這個問題建議在多線程中使用FMDatabaseQueue對象,相比FMDatabase而言,它是線程安全的。
創建FMDatabaseQueue的方法是類似的,調用databaseQueueWithPath:方法即可。注意這里不需要調用打開操作。
-(void)openDb:(NSString *)dbname{ //取得數據庫保存路徑,通常保存沙盒Documents目錄 NSString *directory=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; NSLog(@"%@",directory); NSString *filePath=[directory stringByAppendingPathComponent:dbname]; //創建FMDatabaseQueue對象 self.database=[FMDatabaseQueue databaseQueueWithPath:filePath];}
然后所有的增刪改查操作調用FMDatabaseQueue的inDatabase:方法在block中執行操作sql語句即可。
-(void)executeNonQuery:(NSString *)sql{ //執行更新sql語句,用于插入、修改、刪除 [self.database inDatabase:^(FMDatabase *db) { [db executeUpdate:sql]; }];}-(NSArray *)executeQuery:(NSString *)sql{ NSMutableArray *array=[NSMutableArray array]; [self.database inDatabase:^(FMDatabase *db) { //執行查詢sql語句 FMResultSet *result= [db executeQuery:sql]; while (result.next) { NSMutableDictionary *dic=[NSMutableDictionary dictionary]; for (int i=0; i<result.columnCount; ++i) { dic[[result columnNameForIndex:i]]=[result stringForColumnIndex:i]; } [array addObject:dic]; } }]; return array;}
之所以將事務放到FMDB中去說并不是因為只有FMDB才支持事務,而是因為FMDB將其封裝成了幾個方法來調用,不用自己寫對應的sql而已。其實在在使用libsqlite3操作數據庫時也是原生支持事務的(因為這里的事務是基于數據庫的,FMDB還是使用的SQLite數據庫),只要在執行sql語句前加上“begin transaction;”執行完之后執行“commit transaction;”或者“rollback transaction;”進行提交或回滾即可。另外在Core Data中大家也可以發現,所有的增、刪、改操作之后必須調用上下文的保存方法,其實本身就提供了事務的支持,只要不調用保存方法,之前所有的操作是不會提交的。在FMDB中FMDatabase有beginTransaction、commit、rollback三個方法進行開啟事務、提交事務和回滾事務。
新聞熱點
疑難解答