利用数据库存储是ios进行数据持久化的三个方法之一,由于sqlite的轻量、易用而备受欢迎,现在我们可以将数据库的一些操作进行封装,方便以后重用。
——————————————————————————————————————
一、Sqlite基础
SQLite是一种关系型数据库,因为其轻量易操作而成为当前移动客户端的主流数据库。使用SQLite可以存储大量数据并且易于对数据进行管理、查询。
在SQLite中有以下几种基本类型
TEXT:文本型
INTEGER:整形
REAL:实数型
BLOB:二进制数据
NULL:空值
以上是5种“基本数据类型”,其实sqlite3也可以接受其他的一些复杂类型,例如date、time等。不过在ios开发中完全可以用TEXT代替任意类型,非常方便。
另外,SQLite采用动态数据类型,即可以根据存入的值进行自动的判断,换句话说,在建表时完全可以将类型忽略,不过为了可读性,我们一般不会省略数据类型,同时也不建议省略。
二、使用sqlite进行开发
要利用sqlite就得先将其导入到项目中,添加sqlite对应的库libsqlite3.dylib
然后在用到的文件中引入头文件
<span style="font-size:14px;">#import <sqlite3.h>
</span>
添加好以后就可以利用sqlite的相关函数进行操作了。sqlite为我们提供了不少方法,不过这些方法都是C层级的,常用的几个类型和方法有:
//sqlite常用的数据类型与方法
ADT SQLite {
attribute:
sqlite3 *sqlite;
sqlite3_stmt *stmt;
methods:
//打开数据库,这是数据库操作的第一步,第一个参数代表文件名,第二个参数为数据库句柄指针的地址
void sqlite3_open(const char *filename, sqlite3 **ppDb);
//编译数据库语句,为查询做准备,第二个参数为sql语句(UTF-8),第三个为长度,第四个是stmt类型变量
int sqlite3_prepare(sqlite3 *db,const char *zSql,int nByte,sqlite3_stmt **ppStmt,const char **pzTail);
//为占位符绑定数据,根据绑定的数据类型不同调用的函数也不同
//之前也说过ios中多用text,所以这里将绑定text的函数搬上来
//前三个参数分别代表stmt变量、被绑定占位符的位置(从1开始)、值(UTF-8)
int sqlite3_bind_text(sqlite3_stmt*, int, const char*, int n, void(*)(void*));
//当sql语句被绑定好数据后就可以调用该函数执行该语句了,这个其实是对很多操作的封装,可以方便的执行语句。不过注意此函数执行的是非查询操作
int sqlite3_exec(sqlite3*, const char *sql, sqlite_callback, void*, char**);
//“执行”编译好的sql语句并返回结果,有查询结果将并返回SQLITE_ROW,查询中如果所有行查询完毕后返回SQLITE_DONE,如果有错误则返回SQLITE_ERROR
int sqlite3_step(sqlite3_stmt*);
//销毁一个stmt,在关闭数据库前必须进行此操作。
int sqlite3_finalize(sqlite3_stmt*);
}
这里仿照了一下数据结构ADT的定义,只是将常用的(或者说,接下来要用的)属性和方法给写了出来。上面说的对字符编码的要求是UTF-8,当然也有16版本。这些函数的解释都在注释中,如果还想深入了解可以查一下相关API。
通常,我们在用数据库的时候会先用open函数建表,然后实现其插入、删除、修改、查询等功能。实现方式也比较简单,加之部分与后文重复,这里就不暂时不给出实现代码了。现在我们来将这些方法封装一下,以后可以更加方便的使用数据库。
三、封装数据库操作
数据库的操作无非有建表、插入、删除、修改、查询。尽管每种数据模型每次操作的数据库语句都不一样,但是其他的操作基本上是一致的,我们可以将数据库语句做参数,将对应的功能进行封装。
首先获取沙盒目录,以后会在此建表。
<span style="font-size:14px;">#define kFileName @"RSSReader.sqlite"
- (NSString *)filePath
{
NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *filePath = [file stringByAppendingPathComponent:kFileName];
//NSLog(@"%@", file);
return filePath;
}</span>
接下来建表。建表的简单思路是:
打开数据库——错误处理——处理数据库语句——错误处理——关闭数据库——完成。
在以上步骤中不同表的不同之处仅在于数据库语句,因此将其作为参数。如下
- (void)createTable:(NSString *)sqlString
{
sqlite3 *sqlite = nil;
int result = sqlite3_open([self.filePath UTF8String], &sqlite);
if (result != SQLITE_OK) {
//NSLog(@"打开数据库失败");
sqlite3_close(sqlite);
}
char *errMsg;
result = sqlite3_exec(sqlite, [sqlString UTF8String], NULL, NULL, &errMsg);
if (result != SQLITE_OK) {
//NSLog(@"创建表失败:%s", errMsg);
sqlite3_close(sqlite);
}
//NSLog(@"创建表成功");
sqlite3_close(sqlite);
}
其次是插入、删除与修改。之所以这三个放在一起说是因为这三个操作的操作流程十分相似。可以将他们定义成一种方法:dealSQL:withParams:。操作的流程为:
打开数据库——错误处理——编译SQL语句——错误处理
——绑定数据并执行SQL语句——关闭数据库——完成。
具体实现代码:
- (BOOL)dealSql:(NSString *)sqlString params:(NSArray *)params
{
sqlite3 *sqlite = nil;
sqlite3_stmt *stmt = nil;
//打开数据库
int result = sqlite3_open([self.filePath UTF8String], &sqlite);
if (result != SQLITE_OK) {
//NSLog(@"打开数据库失败");
sqlite3_close(sqlite);
return NO;
}
//编译SQL语句
result = sqlite3_prepare(sqlite, [sqlString UTF8String], -1, &stmt, NULL);
if (result != SQLITE_OK) {
//NSLog(@"数据库语句编译失败");
sqlite3_close(sqlite);
return NO;
}
//绑定数据并执行sql语句
for (int i = 0; i < params.count; i++) {
NSString *value = [params objectAtIndex:i];
sqlite3_bind_text(stmt, i + 1, [value UTF8String], -1, NULL);
}
result = sqlite3_step(stmt);
if (result == SQLITE_ERROR) {
//NSLog(@"数据库操作失败");
sqlite3_close(sqlite);
return NO;
}
//关闭数据库
sqlite3_finalize(stmt);
sqlite3_close(sqlite);
return YES;
}
然后留给用户三个接口:
<span style="font-size:14px;">/**插入数据, params:字符串数组*/
- (BOOL)insertData:(NSString *)sqlString params:(NSArray *)params;
/**删除数据, params:字符串数组*/
- (BOOL)deleteData:(NSString *)sqlString params:(NSArray *)params;
/**更改数据, params:字符串数组*/
- (BOOL)modifyData:(NSString *)sqlString params:(NSArray *)params;</span>
这三个方法的内部实现都是调用刚才的deal方法即可
<span style="font-size:14px;">- (BOOL)deleteData:(NSString *)sqlString params:(NSArray *)params
{
return [self dealSql:sqlString params:params];
}
- (BOOL)insertData:(NSString *)sqlString params:(NSArray *)params
{
return [self dealSql:sqlString params:params];
}
- (BOOL)modifyData:(NSString *)sqlString params:(NSArray *)params
{
return [self dealSql:sqlString params:params];
}</span>
刚才也说过,之所以把查询单拿出来,是因为它跟上述操作有所不同——必须知道你的SQL语句中要查询变量的个数,并且使用step函数,然后通过该函数的返回结果决定下一步操作。具体来说,如果step返回SELECT_ROW则说明查询到一条数据,可以继续调用step函数接着查询,如果返回SELECT_ERROR说明有错误,如果返回SELECT_DONE说明查询结束,利用循环语句可以实现这一功能。
代码如下:
<span style="font-size:14px;">- (NSMutableArray *)selectData:(NSString *)sqlString columnCount:(NSInteger)count
{
sqlite3 *sqlite = nil;
sqlite3_stmt *stmt = nil;
//打开数据库
int result = sqlite3_open([self.filePath UTF8String], &sqlite);
if (result != SQLITE_OK) {
//NSLog(@"打开数据库失败");
sqlite3_close(sqlite);
return nil;
}
//编译SQL语句
result = sqlite3_prepare(sqlite, [sqlString UTF8String], -1, &stmt, NULL);
if (result != SQLITE_OK) {
//NSLog(@"数据库语句编译失败");
sqlite3_close(sqlite);
return nil;
}
NSMutableArray *resultArray = [NSMutableArray array];
result = sqlite3_step(stmt);
while (result == SQLITE_ROW) {
NSMutableArray *everyData = [NSMutableArray array];
for (int i = 0; i < count; i++) {
NSString *everyColumn = [NSString stringWithCString:(char *)sqlite3_column_text(stmt, i) encoding:NSUTF8StringEncoding];
[everyData addObject:everyColumn];
}
[resultArray addObject:everyData];
result = sqlite3_step(stmt);
}
//关闭数据库
sqlite3_finalize(stmt);
sqlite3_close(sqlite);
return resultArray;
}</span>
这样我们的数据库操作就封装好了,如果以后有其他需要可以自己按照相同思路添加。
四、使用封装好的库
既然已经将这些操作都封装好了,那就实际操作一下吧。这里拿我自己的RSS应用来说明。
其实刚才的类只是一个“抽象类”,因为并没有实际的sql语句。再想象一下,每一个表对应的sql语句不同,所以我们可以继承这个抽象类,加入自己的方法来提供sql语句。例如在RSS应用中,我一共设了两个表:存RSS源的表和存每个RSS源下文章的RSSList表。这里用RSS表来举例,接口文件中定义如下:
<span style="font-size:14px;">@class RSSModel;
@interface RSSSqlite : ICESqlite
+(id)sharedInstance;
- (void)createRSSTable;
- (BOOL)addRSS:(RSSModel *)model;
- (BOOL)deleteRSS:(RSSModel *)model;
- (NSMutableArray *)selectRSS;</span>
该类被设置为一个单例类,在下面四个方法中分别给出四条sql语句,然后调用继承下来的刚才实现好的哪些方法就可以方便的进行数据库操作了。
.m:
<span style="font-size:14px;">- (void)createRSSTable
{
NSString *sql = @"CREATE TABLE IF NOT EXISTS RSSArtical(link TEXT primary key,name TEXT,date TEXT,category TEXT,imageUrlString TEXT)";
[self createTable:sql];
}
- (BOOL)addRSS:(RSSModel *)model
{
NSString *sql = @"INSERT INTO RSSArtical(link,name,date,category,imageUrlString) VALUES(?,?,?,?,?)";
NSArray *array = [NSArray arrayWithObjects:model.link, model.name, model.date, model.category, model.imageUrlString, nil];
return [self insertData:sql params:array];
}
- (BOOL)deleteRSS:(RSSModel *)model
{
NSString *sql = @"DELETE FROM RSSArtical WHERE link=?";
NSArray *array = [NSArray arrayWithObject:model.link];
return [self deleteData:sql params:array];
}
- (NSMutableArray *)selectRSS
{
NSString *sql = @"SELECT link,name,date,category,imageUrlString FROM RSSArtical";
NSArray *data = [self selectData:sql columnCount:5];
NSMutableArray *rssArticals = [NSMutableArray array];
for (NSArray *array in data) {
NSString *link = array[0];
NSString *name = array[1];
NSString *date = array[2];
NSString *category = array[3];
NSString *imageUrlString = array[4];
RSSModel *model = [[RSSModel alloc] initWithlink:link name:name date:date category:category imageUrlString:imageUrlString];
[rssArticals addObject:model];
}
return rssArticals;
}</span>
最后再说一点吧,建表操作只能执行一次,在ios中有很多方法可以实现这个功能,其中可以利用判断是否为用户首次开启该应用来实现。而这一功能也是非常实用的。
<span style="font-size:14px;">if ([[NSUserDefaults standardUserDefaults] boolForKey:@"firstRSS"]) {
//NSLog(@"第一次登陆,创建RSS表");
[[RSSSqlite sharedInstance] createRSSTable];
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"firstRSS"];
}</span>