iOS 数据库操作
1. SQLite
稍后要介绍的FMDB和WCDB都是基于SQLite的封装。当然,iOS也可以直接使用SQLite,系统中内置了libsqlite的库。
1.1 配置工作
首先,要添加库文件libsqlite3.tbd,

其次,导入头文件 sqlite3.h,
#import <sqlite3.h>
1.2 使用sqlite的基本过程
//生成路径
+(NSString *)path{
NSArray *documentArr = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentPath = [documentArr firstObject];
// crylown.db 为数据库的名字
NSString *path = [NSString stringWithFormat:@"%@/student.sqlite",documentPath];
NSLog(@"位置path == %@",path);
return path;
}
//创建/打开数据库
-(void)createDataBase{
int databaseResult = sqlite3_open([[SqliteObject path] UTF8String], &database);
if (databaseResult == SQLITE_OK) {
[self createForm];
}
else {
NSLog(@"创建/打开数据库失败,%d",databaseResult);
}
}
//创建表
-(void)createForm{
char *error = NULL;
// 建表格式: create table if not exists 表名 (列名 类型,....) 注: 如需生成默认增加的id: id integer primary key autoincrement
const char *createSQL = "create table if not exists t_students (id integer primary key autoincrement, name text, age integer, sex text)";
// 执行sql语句
/**
第1个参数:数据库对象
第2个参数:sql语句
第3个参数:查询时候用到的一个结果集闭包
第4个参数:用不到
第5个参数:错误信息
*/
int tableResult = sqlite3_exec(database, createSQL, NULL, NULL, &error);
if (tableResult == SQLITE_OK) {
NSLog(@"创建成功");
}
else {
NSLog(@"创建表失败:%s",error);
}
}
// 插入数据
- (void)insertData:(NSString*)name ageinteger:(int)age sexStr:(NSString*)sex {
// 拼接 sql 语句
NSString *sql = [NSString stringWithFormat:@"insert into t_students (name,age,sex) values ('%@',%d,'%@');",name,age,sex];
// 执行 sql 语句
char *errMsg = NULL;
int result = sqlite3_exec(database, sql.UTF8String, NULL, NULL, &errMsg);
if (result == SQLITE_OK) {
NSLog(@"插入数据成功 - %@",name);
}
else {
NSLog(@"插入数据失败 - %s",errMsg);
}
}
// 查询操作
- (void)sqlData {
// sql语句
const char *sql="SELECT id,name,age,sex FROM t_students WHERE age<20;";
sqlite3_stmt *stmt = NULL;
/**
第1个参数:一个已经打开的数据库对象
第2个参数:sql语句
第3个参数:参数2中取出多少字节的长度,-1 自动计算,\0停止取出
第4个参数:准备语句
第5个参数:通过参数3,取出参数2的长度字节之后,剩下的字符串
*/
// 进行查询前的准备工作
if (sqlite3_prepare_v2(database, sql, -1, &stmt, NULL) == SQLITE_OK) { // sql语句没有问题
NSLog(@"sql语句没有问题");
// 每调用一次sqlite3_step函数,stmt就会指向下一条记录
while (sqlite3_step(stmt) == SQLITE_ROW) { // 找到一条记录
// 取出数据
int ID = sqlite3_column_int(stmt, 0); // 取出第0列字段的值
const unsigned char *name = sqlite3_column_text(stmt, 1); // 取出第1列字段的值
int age = sqlite3_column_int(stmt, 2); // 取出第2列字段的值
const unsigned char *sex = sqlite3_column_text(stmt, 3);
printf("%d %s %d\n %s",ID,name,age,sex);
}
}
else {
NSLog(@"查询语句有问题");
}
}
//修改数据
-(void)updateData {
//其实Sqlite的数据插入,修改,删除执行的方法都是一样的只是执行的sql语句不一样,想知道sql的更多语句操作自行百度了,比较多这里就不讲解了,只介绍一些基本的操作方法。
//sqlite3数据(把年龄大于60的学生名字全部改成‘哈哈’)
NSString *sql = @"update t_students set name = '哈哈' where age > 60";
char *errorMesg = NULL;
int result = sqlite3_exec(database, sql.UTF8String, NULL, NULL, &errorMesg);
if (result == SQLITE_OK) {
NSLog(@"更改成功");
}
else {
NSLog(@"更改失败");
}
//然后执行查询语句就能看到更改后的效果了
}
//删除数据
-(void)deleteData {
//删除表中年龄大于60的学生数据
NSString *sql = @"delete from t_students where age >= 60";
char *errorMesg = NULL;
int result = sqlite3_exec(database, sql.UTF8String, NULL, NULL, &errorMesg);
if (result == SQLITE_OK) {
NSLog(@"删除成功");
}
else {
NSLog(@"删除失败");
}
}
1.3 SQLite基础
注意:SQL语句不区分大小写(CREATE = create);每条语句以分号(;)结尾;关键字建议大写。
- 一般的使用方法是创建一个单例管理类,专门管理数据库的操作。
- 开发数据库的步骤:
a.建立数据库 → 新加了本地数据库文件。
b.创建数据表 → 每张数据表存储一类数据
c.利用SQL命令实现增删查改,并在UI显示
SQL命令:
a) DDL–数据定义语言:CREATE(创建新表)、ALTER(修改更新表)、DROP(删除整个表)
b) DML-数据操作语言:INSERT(插入)、UPDATE(更新)、DELETE(删除)
c) DQL-数据查询语言:SELECT(查询)
SQLite C语言库操作
a) 在工程中导入SQlite框架:libsqlite3.dylib
b) sqlite3_open打开数据库,如果数据库不存在会新建,返回数据库的句柄
c) sqlite3_exec参数为数据库句柄,执行SQL命令,实现创建Table,增删查改操作。
实质就是sqlite3_prepare(把字符串转化为SQL命令对象)和sqlite3_step(执行SQL命令对象)和sqlite3_finalize(销毁SQL对象,否则内存泄漏)的结合体。sqlite3_column取指定列数据,用于查询数据,包括获取sqlite3_column_name列名、sqlite3_column_type数据类型等
d) sqlite3_close关闭数据库,参数为句柄,不过一般打开后就不会关闭数据库,提高效率。
https://www.cnblogs.com/zhun/p/5543268.html
https://www.jianshu.com/p/4a0e6773c694
https://www.jianshu.com/p/2333fad79f2f
https://blog.csdn.net/swanzhu/article/details/48475039
由于Apple提供的CoreData框架差强人意,使得开发者们纷纷将目光投向开源社区,寻找更好的存储方案。
2. FMDB
FMDB将C语言的iOS系统的SQLite数据库的操作代码用OC进行封装,面向对象,容易理解和使用;对比苹果自带的Core Data框架,更加轻量级和灵活,并且提供了线程不安全的解决方案。
2.1 使用说明
cocopads 引入 FMDB 库
pod 'FMDB'
-
三个重要的类:
i. FMDatabase:一个FMDatabase对象就代表一个单独的SQLite数据库;
ii. FMResultSet:使用FMDatabase执行查询后,得到结果集;
iii. FMDatabaseQueue:用于解决线程不安全的类,避免每个线程创建一个数据库,导致数据库冗余; -
常用方法:
i. [FMDatabaseQueue databaseQueueWithPath:dbPath]; // 创建数据库
ii. [db executeQuery:sqlStr] //执行查询SQL语句,在FMDatabaseQueue中执行
iii. [db executeUpdate:sqlStr] // 执行SQL(增删改)语句 -
封装FMDB
a) 首先,一个单例管理工具类
b) 封装执行sql语句的方法和查询数据库的方法、批量处理sql语句的方法、
c) 使用3个队列,防止数据嵌套时,会照成死循环的问题。
2.2 打开数据库
通过指定SQLite数据库文件路径来创建FMDatabase对象
// 1..创建数据库对象
FMDatabase *db = [FMDatabase databaseWithPath:path];
// 2.打开数据库
if ([db open]) {
// do something
} else {
DLog(@"fail to open database");
}
/*
文件路径有三种情况:
1)具体文件路径:如果不存在会自动创建,建议使用绝对路径;
2)空字符串@"":会在临时目录创建一个空的数据库,当FMDatabase连接关闭时,数据库文件也被删除;
3)nil:会创建一个内存中临时数据库,当FMDatabase连接关闭时,数据库会被销毁。
【备注】path 可以是相对路径,也可以是绝对路径。
/*
建表操作
NSString *createTableSqlString = @"CREATE TABLE IF NOT EXISTS t_student (id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, age integer NOT NULL)";
[db executeUpdate:createTableSqlString];
2.3 数据库的增删改查
使用executeUpdate: 方法执行更新,
//// 1.示例INSERT(写入数据)
//不确定的参数用?来占位
NSString *sql = @"insert into t_student (name, age) values (?, ?)";
[db executeUpdate:sql, @"zhangsan", [NSNumber numberWithInt:18]];
//// 2.示例DELETE(删除数据)
NSString *sql = @"delete from t_student where id = ?";
[db executeUpdate:sql, [NSNumber numberWithInt:1]];
//// 3.示例UPDATE(更改数据)
NSString *sql = @"update t_student set name = "heiheihei" where id = ?";
[db executeUpdate:sql, [NSNumber numberWithInt:1]];
// -(BOOL)executeUpdateWithFormat:(NSString*)format, ...
//不确定的参数用%@,%d等来占位
NSString *sql = @"insert into t_student (name,age) values (%@,%i)";
[db executeUpdateWithFormat:sql, @"zhangsan", 18];
// -(BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)
NSDictionary *studentDict = [NSDictionary dictionaryWithObjectsAndKeys:@"lisi", @"name", @"18", @"age", nil];
[db executeUpdate:@"insert into t_student (name, age) values (:name, :age)" withParameterDictionary:studentDict];
//// 4.执行查询操作
//查询方法
// 1)(FMResultSet *)executeQuery:(NSString*)sql, ...
// 2)(FMResultSet *)executeQueryWithFormat:(NSString*)format, ...
// 查询
NSString *sql = @"select id, name, age FROM t_student";
FMResultSet *rs = [db executeQuery:sql];
while ([rs next]) {
int id = [rs intForColumnIndex:0];
NSString *name = [rs stringForColumnIndex:1];
int age = [rs intForColumnIndex:2];
NSDictionary *studentDict = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:id], @"id", name, @"name", [NSNumber numberWithInt:age], @"age", nil];
[studentArray addObject:studentDict];
}
2.4 多语句和批处理
FMDatabase 可以通过 -executeStatements:withResultBlock: 方法在一个字符串中执行多语句。
NSString *sql = @"CREATE TABLE IF NOT EXISTS bulktest1 (id integer PRIMARY KEY AUTOINCREMENT, x text);"
"CREATE TABLE IF NOT EXISTS bulktest2 (id integer PRIMARY KEY AUTOINCREMENT, y text);"
"CREATE table IF NOT EXISTS bulktest3 (id integer primary key autoincrement, z text);"
"insert into bulktest1 (x) values ('XXX');"
"insert into bulktest2 (y) values ('YYY');"
"insert into bulktest3 (z) values ('ZZZ');"
;
result = [db executeStatements:sql];
sql = @"select count(*) as count from bulktest1;"
"select count(*) as count from bulktest2;"
"select count(*) as count from bulktest3;";
result = [db executeStatements:sql withResultBlock:^int(NSDictionary *resultsDictionary) {
NSLog(@"dictionary=%@", resultsDictionary);
return 0;
}];
2.5 队列和线程安全
在多线程中同时使用 FMDatabase 单例是极其错误的想法,会导致每个线程创建一个 FMDatabase 对象。不要跨线程使用单例,也不要同时跨多线程,不然会奔溃或者异常。因此不要实例化一个 FMDatabase 单例来跨线程使用,相反使用FMDatabaseQueue,下面就是它的使用方法:
//// 创建队列
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
// 示例:
[queue inDatabase:^(FMDatabase *db) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
FMResultSet *rs = [db executeQuery:@"select * from foo"];
while ([rs next]) {
...
}
}];
把操作放在事务中也很简单,示例:
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
if (whoopsSomethingWrongHappened) {
*rollback = YES;
return;
}
// ...
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:4]];
}];
很好的示例:
https://www.cnblogs.com/hero11223/p/6057186.html
fmdb使用中问题调试
http://blog.devtang.com/2012/04/22/use-fmdb/
http://www.hcios.com/archives/921
3. WCDB
转自:https://cloud.tencent.com/developer/article/2256743
目前移动端数据库方案按其实现可分为两类,
-
关系型数据库,代表有CoreData、FMDB等。
CoreData 它是苹果内建框架,和Xcode深度结合,可以很方便进行ORM;但其上手学习成本较高,不容易掌握。稳定性也堪忧,很容易crash;多线程的支持也比较鸡肋。
FMDB 它基于SQLite封装,对于有SQLite和ObjC基础的开发者来说,简单易懂,可以直接上手;而缺点也正是在此,FMDB只是将SQLite的C接口封装成了ObjC接口,没有做太多别的优化,即所谓的胶水代码(Glue Code)。使用过程需要用大量的代码拼接SQL、拼装Object,并不方便。 -
key-value数据库,代表有Realm、LevelDB、RocksDB等。
Realm 因其在各平台封装、优化的优势,比较受移动开发者的欢迎。对于iOS开发者,key-value的实现直接易懂,可以像使用NSDictionary一样使用Realm。并且ORM彻底,省去了拼装Object的过程。但其对代码侵入性很强,Realm要求类继承RLMObject的基类。这对于单继承的ObjC,意味着不能再继承其他自定义的子类。同时,key-value数据库对较为复杂的查询场景也比较无力。
可见,各个方案都有其独特的优势及劣势,没有最好的,只有最适合的。
好的数据库应满足:
- 高效: 增删改查的高效是数据库最基本的要求。除此之外,我们还希望能够支持多个线程高并发地操作数据库,以应对微信频繁收发消息的场景。
- 易用:SQLite本不是一个易用的组件:为了完成一个查询,往往我们需要写很多拼接字符串、组装Object的胶水代码。这些代码冗长繁杂,而且容易出错,我们希望组件能统一完成这些任务。
- 完整:数据库操作是一个复杂的场景,我们希望数据库组件能完整覆盖各种场景。包括数据库损坏、监控统计、复杂的查询、反注入等。
显然,上述各个方案都不能完全满足微信的需求。
于是,我们造了这个“轮子” - WCDB-iOS/Mac
WCDB-iOS/Mac
WCDB-iOS/Mac(以下简称WCDB,均指代WCDB的iOS/Mac版本),是一个基于SQLite封装的Objective-C++数据库组件,提供了如下功能:
- 便捷的ORM和CRUD接口:通过WCDB,开发者可以便捷地定义数据库表和索引,并且无须写一坨胶水代码拼装对象。
- WINQ(WCDB语言集成查询):通过WINQ,开发者无须拼接字符串,即可完成SQL的条件、排序、过滤等等语句。
-
多线程高并发:基本的增删查改等接口都支持多线程访问,开发者无需操心线程安全问题。
- 线程间读与读、读与写操作均支持并发执行。
- 写与写操作串行执行,并且有基于SQLite源码优化的性能提升。可参考我们分享的另一篇文章《微信iOS SQLite源码优化实践》
- 损坏修复:数据库损坏一直是个难题,WCDB内置了我们自研的修复工具WCDBRepair。同样可参考我们分享的另一篇文章《微信 SQLite 数据库修复实践》
- 统计分析:WCDB提供接口直接获取SQL的执行耗时,可用于监控性能。
- 反注入:WCDB框架层防止了SQL注入,以避免恶意信息危害用户数据。
- ...
WCDB覆盖了数据库使用的绝大部分场景,且经过微信海量用户的验证,并将持续不断地增加新的能力。
本文是WCDB系列文章的第一篇,主要介绍WCDB-iOS/Mac的基本用法,包含:
- ORM、CRUD与Transaction
- WINQ
- 高级用法
ORM
在WCDB内,ORM(Object Relational Mapping)是指
- 将一个ObjC的类,映射到数据库的表和索引;
- 将类的property,映射到数据库表的字段;
这一过程。通过ORM,可以达到直接通过Object进行数据库操作,省去拼装过程的目的。
WCDB通过内建的宏实现ORM的功能。如下


对于一个已有的ObjC类,
- 引用WCDB框架头文件#import <WCDB/WCDB.h>,并定义类遵循WCTTableCoding协议
- WCDB_PROPERTY用于在头文件中声明绑定到数据库表的字段。
- WCDB_IMPLEMENTATION,用于在类文件中定义绑定到数据库表的类。同时,该宏内实现了WCTTableCoding。因此,开发者无须添加更多的代码来完成WCTTableCoding的接口
- WCDB_SYNTHESIZE,用于在类文件中定义绑定到数据库表的字段。
简单几行代码,就完成了将类和需要的字段绑定到数据库表的过程。这三个宏在名称和使用习惯上,也都和定义一个ObjC类相似,以此便于记忆。
除此之外,WCDB还提供了许多可选的宏,用于定义数据库索引、约束等,如:
- WCDB_PRIMARY用于定义主键
- WCDB_INDEX用于定义索引
- WCDB_UNIQUE用于定义唯一约束
- WCDB_NOT_NULL用于定义非空约束
- ...
定义完成后,只需要调用createTableAndIndexesOfName:withClass:接口,即可创建表和索引。

接口会根据ORM的定义,创建对应表和索引。
CRUD
得益于ORM的定义,WCDB可以直接进行通过object进行增删改查(CRUD)操作。




Transaction
WCDB内可通过两种方式执行Transaction(事务),一是runTransaction:接口

这种方式要求数据库操作在一个BLOCK内完成,简单易用。
另一种方式则是获取WCTTransaction对象

WCTTransaction对象可以在类或函数间传递,因此这种方式也更具灵活性。
WINQ
有心的同学可能会注意到上述例子中的一些特殊语法:
- where:Message.localID>0
- onProperties:Message.content
- orderBy:Message.localID.order()
这个便是WINQ。
WINQ(WCDB Integrated Query,音'wink'),即WCDB集成查询,是将自然查询的SQL集成到WCDB框架中的技术,基于C++实现。
传统的SQL语句,通常是开发者拼接字符串完成。这种方式不仅繁琐、易错,而且出错后很难定位到问题所在。同时也容易给SQL注入留下可乘之机。
而WINQ将查询语言集成到了C++中,可以通过类似函数调用的方式来写SQL查询。借用IDE的代码提示和编译器的语法检查,达到易用、纠错的效果。
对于一个已绑定ORM的类,可以通过className.propertyName的方式,获得数据库内字段的映射,以此书写SQL的条件、排序、过滤等等所有语句。如下是几个例子:

由于WINQ通过接口调用实现SQL查询,因此在书写过程中会有IDE的代码提示和编译器的语法检查,从而提升开发效率,避免写错。


WINQ的接口包括但不限于:
- 一元操作符:+、-、!等
- 二元操作符:||、&&、+、-、*、/、|、&、<<、>>、<、<=、==、!=、>、>=等
- 范围比较:IN、BETWEEN等
- 字符串匹配:LIKE、GLOB、MATCH、REGEXP等
- 聚合函数:AVG、COUNT、MAX、MIN、SUM等
- ...
凡是SQLite支持的语法规则,WINQ基本都有其对应的接口。且接口名称与SQLite的语法规则基本保持一致。对于熟悉SQL的开发者,无须特别学习即可立刻上手使用。
高级用法
as重定向
基于ORM的支持,我们可以从数据库直接取出一个Object。然而,有时候需要取出并非是某个字段,而是有一些组合。例如:

这段代码从数据库中取出了消息的最新的修改时间,并以此将此时间作为消息的创建时间,新建了一个message。这种情况下,就可以使用as重定向。
as重定向,它可以将一个查询结果重定向到某一个字段,如下:

通过as(Message.createTime)的语法,将查询结果重新指向了createTime。因此只需一行代码便可完成原来的任务。
链式调用
链式调用是指对象的接口返回一个对象,从而允许在单个语句中将调用链接在一起,而不需要变量来存储中间结果。
WCDB对于增删改查操作,都提供了对应的类以实现链式调用
- WCTInsert
- WCTDelete
- WCTUpdate
- WCTSelect
- WCTRowSelect
- WCTMultiSelect

where、orderBy、limit等接口的返回值均为self,因此可以通过链式调用,更自然更灵活的写出对应的查询。
传统的接口方便快捷,可以直接获得操作结果;链式接口则更具灵活性,开发者可以获取数据库操作的耗时、错误信息;也可以通过遍历逐个生成object。

WCDB内同时支持这两种接口,优势互补,开发者可以根据需求,选择使用。
多表查询
SQLite支持联表查询,在某些特定的场景下,可以起到优化性能、简化表结构的作用。
WCDB同样提供了对应的接口,并在ORM的支持下,通过WCTMultiSelect的链式接口,可以同时从表中取出多个类的对象。

类字段绑定
在ORM中,我们通过宏,将ObjC类的property绑定为数据库的一个字段。但并非所有property的类型都能绑定到字段。
WCDB内置支持的类型有:
- const char*的C字符串类型
- 包括但不限于int、unsigned、long、unsigned long、long long、unsigned long long等所有基于整型的C基本类型
- 包括但不限于float、double、long double等所有基于浮点型的C基本类型
- enum及所有基于枚举型的C基本类型
- NSString、NSMutableString
- NSData、NSMutableData
- NSArray、NSMutableArray
- NSDictionary、NSMutableDictionary
- NSSet、NSMutableSet
- NSValue
- NSDate
- NSNumber
- NSURL
然而,内置支持得再多,也不可能完全覆盖开发者所有的需求。因此WCDB支持开发者自定义类字段绑定。
类只需实现WCTColumnCoding协议,即可支持绑定。

- columnTypeForWCDB接口定义类对应数据库中的类型
- unarchiveWithWCTValue:接口定义从数据库类型反序列化到类的转换方式
- archivedWCTValue接口定义从类序列化到数据库类型的转换方式
为了简化定义,WCDB提供了文件模版来创建类字段绑定。
- 首先需要安装文件模版。该模版的安装脚本集成在WCDB的编译脚本中,只需编译一次WCDB,就会自动安装文件模版。安装完成后重启Xcode,新建文件,即可看到对应的文件模版

- 选择WCTColumnCoding

- Class:需要进行字段绑定的类,这里以NSDate为例
- Language:WCDB支持绑定ObjC类和C++类,这里选择Objective-C
- Type In DataBase:类对应数据库中的类型。包括
- WCTColumnTypeInteger32
- WCTColumnTypeInteger64
- WCTColumnTypeDouble
- WCTColumnTypeString
- WCTColumnTypeBinary
- 我们知道NSDate是遵循NSCoding协议的,因此这里选择了Binary类型。即,将NSDate以二进制数据的形式存到数据库中。完成后会自动创建如下的文件模版:

- 然后只需将NSDate和NSData互相转换的方式填上去即可。如下:

总结
WCDB通过ORM和WINQ,体现了其易用性上的优势,使得数据库操作不再繁杂。同时,通过链式调用,开发者也能够方便地获取数据库操作的耗时等性能信息。而高级用法则扩展了WCDB的功能和用法。