FMDB使用,事务,多线程,加密等比较全面的用法。
一:简单使用
既然FMBD是对数据库的封装,那基本功能应该包括 增、删、改、查。
主要使用到的类为FMDatabase(数据库)FMResultSet(查询的结果)。
其中增、删、改,三个方法都是使用FMDatabase类的executeUpdate方法也就是都算作数据的更新,
而查使用的是FMDatabase类的executeQuery方法查询结果为FMResultSet,但在这还是将增,删,改都单独提了方法,方便区分。
一般使用时都会创建一个管理类对这些方法进行统一管理,需要注意的是该类应为单利,用来保证操作的是同一个数据库。
这里采用CGD方式创建了单利:创建DataManager类继承NSObject。
在.h文件中:
+ (id)shareDataManager;
声明一个获取单利对象的方法
在.m文件中:
#import "DataManager.h"
static DataManager * manager;
@interface DataManager ()
@property(nonatomic, strong)FMDatabase * db;
@end
@implementation DataManager
+ (id)shareDataManager{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[DataManager alloc] init];
});
return manager;
}
+ (id)allocWithZone:(struct _NSZone *)zone{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [super allocWithZone:zone];
});
return manager;
}
实现获取单利对象的方法,并且声明了一个FMDatabase的成员变量。
接下来就是对FMDatabase的操作:
(1)创建数据库
在DataManager.h文件中声明方法
//创建数据库
- (BOOL)buildDatabase;
在DataManager.m文件中实现该方法
#pragma mark - 创建数据库
- (BOOL)buildDatabase{
NSString * docuPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString * dbPath = [docuPath stringByAppendingPathComponent:@"database.db"];
self.db = [FMDatabase databaseWithPath:dbPath];
BOOL openLFlag = [self.db open];
if (!openLFlag) {
NSLog(@"数据库创建/打开失败");
return NO;
}
}
前两行代码是设置数据库路径及数据库名称,可用xxx.db和xxx.sqlite两种格式
[self.db open]是创建数据库,如果数据库已经存在则打开。
(2)创建表格
一般来讲我们会把创建表格的步骤放在创建数据库里数据库打开成功功能之后
NSString * sql = @"create table if not exists person ('ID' INTEGER PRIMARY KEY AUTOINCREMENT,'name' TEXT NOT NULL,'phone' TEXT NOT NULL,'score' INTGEGER NOT NULL)";
BOOL result = [self.db executeUpdate:sql];
if (result) {
NSLog(@"表格创建成功");
return YES;
}else{
NSLog(@"表格创建失败");
}
return NO;
表示创建一个person表格里面包含四个字段,ID为主键,int类型型数据,插入数据时主键如果不传入则按照上一个的值自动➕1
(3)增
在DataManager.h文件中增加方法
//插入数据
- (BOOL)insertWithSql:(NSString *)sql;
在DataManager.m文件中实现
#pragma mark - 插入数据
- (BOOL)insertWithSql:(NSString *)sql{
BOOL result = [self.db executeUpdate:sql];
if (result) {
NSLog(@"插入表格成功");
return YES;
}else{
NSLog(@"插入表格失败");
}
return NO;
}
如上面所说使用executeUpdate方法实现数据插入。
在ViewController.m中调用:
#pragma mark - 插入数据
- (void)insert{
Person * person = [[Person alloc] init];
person.ID = 5;
person.name = @"小明";
person.phone = @"13232432435";
person.score = 99;
//传入主键ID
NSString * insertSql1 = [NSString stringWithFormat:@"insert into 'person'(ID,name,phone,score) values (%d,'%@','%@',%d)",person.ID,person.name,person.phone,person.score];
[[DataManager shareDataManager] insertWithSql:insertSql1];
//未传主键ID
NSString * insertSql = [NSString stringWithFormat:@"insert into 'person'(name,phone,score) values ('%@','%@',%d)",person.name,person.phone,person.score];
[[DataManager shareDataManager] insertWithSql:insertSql];
}
这个时候可以根据数据库地址在Finder前往(command+shift+g)文件夹,使用Navicat Premium或其它软件打开数据库会发现里
面有个person表格,表格下有两条数据
以上是比较常用的一种方法还有另外三种:
1.
BOOL result = [self.db executeUpdate:@"insert into 'person' (ID,namme,phone,score)values(?,?,?,?)",@100,@"小明",@"13023848239",89];
2.
BOOL result = [self.db executeUpdate:@"insert into 'person' (ID,name.phone,score) values(%ld,%@,%@,%ld)",112,@"小明",@"1324097923748",90];
3.
BOOL result = [self.db executeUpdate:@"insert into 'person' (ID,name,phone,score) values(?,?,?,?)" withArgumentsInArray:@[@111,@"小明",@"213798784",@90]];
(4)删
在DataManager.h中添加方法
//删除数据
- (BOOL)deleteWithSql:(NSString *)sql;
在DataManager.m中实现方法其实和插入的实现方法一样
#pragma mark - 删除数据
- (BOOL)deleteWithSql:(NSString *)sql{
BOOL result = [self.db executeUpdate:sql];
if (result) {
NSLog(@"删除数据成功");
return YES;
}else{
NSLog(@"删除数据失败");
}
return NO;
}
在ViewController.m中调用:
#pragma mark - 删除数据
- (void)delete{
NSString * deleteSql = [NSString stringWithFormat:@"delete from 'person' where ID = %d",5];
[[DataManager shareDataManager] deleteWithSql:deleteSql];
}
以上方法将会删除person表内ID为5的一条数据,调用后刷新数据库,会发现只剩ID为6的一条数据
删除的语句也有另外三种写法:
1、
BOOL result = [self.db executeUpdate:@"delete from 'person' where ID = ?",@5];
2、
BOOL result = [self.db executeUpdate:@"delete from 'person' where ID = %d",5];
3、
BOOL result = [self.db executeUpdate:@"delete from 'person' where ID = ?" withArgumentsInArray:@[@5]];
(5)改
在DataManager.h中添加方法
//更新数据
- (BOOL)updateWithSql:(NSString *)sql;
在DataManager.m中实现方法
#pragma mark - 更新数据
- (BOOL)updateWithSql:(NSString *)sql{
BOOL result = [self.db executeUpdate:sql];
if (result) {
NSLog(@"数据更新成功");
return YES;
}else{
NSLog(@"数据更新失败");
}
return NO;
}
在ViewController.m中调用:
#pragma mark - 更新数据
- (void)update{
NSString * updateSql = [NSString stringWithFormat:@"update 'person' set score = 100 where name = 'Shan'"];
[[DataManager shareDataManager] updateWithSql:updateSql];
}
再执行更新方法之前,可以试着往里面新插入一条name为Shan的数据,设置score为90。
然后执行该语句,会发现name为Shan的那条数据的score值变为了100。
(6)查
这个时候就用到了FMResultSet这个类可以查看该类的.h文件发现里面有很多方法,而我们常用的
- (BOOL)next;
- (int)intForColumnIndex:(int)columnIdx;
- (NSString * _Nullable)stringForColumn:(NSString*)columnName;
等等的
其中第一个方法类似于遍历,而剩下的常用方法基本都为取值,会在应用中见到用法。
还是一样在在DataManager.h中声明方法
//查询数据
- (FMResultSet * )selectWithSql:(NSString *)sql;
此函数带了一个返回值也就是将查询结果返回
在DataManager.m中实现方法
#pragma mark - 查询数据
- (FMResultSet * )selectWithSql:(NSString *)sql{
[self.db open];
FMResultSet * result = [self.db executeQuery:sql];
if (result) {
NSLog(@"查询成功");
return result;
}else{
NSLog(@"没有查询到数据");
}
return nil;
}
在ViewController.m中调用:
#pragma mark - 查找
- (void)select{
NSString * selectSql = [NSString stringWithFormat:@"select * from 'person' where ID = 5"];
FMResultSet * result = [[DataManager shareDataManager] selectWithSql:selectSql];
NSMutableArray * arrayM = @[].mutableCopy;
while ([result next]) {
Person * person = [[Person alloc] init];
person.ID = [result intForColumn:@"ID"];
person.name = [result stringForColumn:@"name"];
person.phone = [result stringForColumn:@"phone"];
person.score = [result intForColumn:@"score"];
[arrayM addObject:person];
}
NSLog(@"%@",arrayM);
}
表示在person表中查询ID为5的数据,因为ID为主键,所以这里最多只会查询到一条数据,
但如果sql语句这样写:@"select * from 'person' where name = '小明'"则会返回所有name为小明的数据,
如果想要查询该表内的全部数据使用 @"select * from 'person'",
还可以查询同时满足多个条件的数据@"select * from 'person' where ID = 5 and name = '小明'"
另外还有模糊查询,集合查询,去重查询等等。
二.事务(Transaction)
首先事务有4大特性,原子性,一致性,隔离性,持久性。反正大家都这么说,至于原因还是需要从代码当中进行验证。
另外事务应用的场景,应该是有大批量数据需要进行插入或修改的时候。
下面写两个方法想表格中插入500条数,其中一个方法使用事务,而另一个就使用普通的插入语句。
同样还是写在了DataManager类中
.h中声明两个方法
//再事务中处理事情
- (void)handleTransaction;
//未在事务中处理
- (void)handleNoTransaction;
.m中实现
#pragma mark - 用事务处理一系列数据库操作
- (void)handleTransaction{
NSString * documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString * dbPath = [documentPath stringByAppendingPathComponent:@"text1.db"];
FMDatabase * db = [FMDatabase databaseWithPath:dbPath];
if (![db open]) {
return;
}
BOOL result = [db executeUpdate:@"create table if not exists text1 ('ID' integer primary key autoincrement,name text,age integer)"];
if (!result) {
NSLog(@"创建表格失败");
}
[db beginTransaction];
NSDate * begin = [NSDate date];
BOOL rollBack = NO;
@try {
for (int i = 0; i < 500; i ++) {
NSString * name = [NSString stringWithFormat:@"text_%d",i];
NSInteger age = i + 10;
NSInteger ID = i;
NSString * insertSql = [NSString stringWithFormat:@"insert into 'text1' (ID,name,age) values(%ld,'%@',%ld)",ID,name,age];
BOOL result = [db executeUpdate:insertSql];
if (!result) {
NSLog(@"插入失败");
rollBack = YES;
return;
}
}
} @catch (NSException *exception) {
rollBack = YES;
} @finally {
if (!rollBack) {
[db commit];
}else{
[db rollback];
}
}
NSDate * end = [NSDate date];
NSTimeInterval time = [end timeIntervalSinceDate:begin];
NSLog(@"在事务中执行插入任务所需时间 === %f",time);
}
#pragma mark - 未使用事务执行一系列操作
- (void)handleNoTransaction{
NSString * ducumentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString * dbPath = [ducumentPath stringByAppendingPathComponent:@"test2.db"];
FMDatabase * db = [FMDatabase databaseWithPath:dbPath];
if (![db open]) {
return;
}
BOOL result = [db executeUpdate:@"create table if not exists text2('ID' integer primary key autoincrement,name text,age integer)"];
if (!result) {
[db close];
}
NSDate * begin = [NSDate date];
for (int i = 0; i < 500; i ++) {
NSString * name = [NSString stringWithFormat:@"text_%d",i];
NSInteger age = i + 10;
NSInteger ID = i;
NSString * insertSql = [NSString stringWithFormat:@"insert into 'text2' (ID,name,age) values(%ld,'%@',%ld)",ID,name,age];
BOOL result = [db executeUpdate:insertSql];
if (!result) {
NSLog(@"插入失败");
return;
}
}
NSDate * end = [NSDate date];
NSTimeInterval time = [end timeIntervalSinceDate:begin];
NSLog(@"不在事务中执行插入任务所需时间===%f",time);
}
ViewController.m中调用
#pragma mark - 在事务中执行
- (void)handleTransaction{
[[DataManager shareDataManager] handleTransaction];
}
#pragma mark - 不在事务中执行
- (void)handleNoTransaction{
[[DataManager shareDataManager] handleNoTransaction];
}
分别调用两个方法会打印如下信息
可以看出如果两个语句都执行成功的话,在事务中执行的语句所花费的时间要远小于不在事务中执行。
(注意不要在语句直接加上过多的NSLog该语句会花费大量时间影响测试结果)
而且打开两个数据库后会发现两个表的数据都是按顺序插入到对应的表中的。
如果在事务的方法中
NSString * insertSql = [NSString stringWithFormat:@"insert into 'text1' (ID,name,age) values(%ld,'%@',%ld)",ID,name,age];
这句代码之下插入如下代码:
if (i == 200 ) {
insertSql = [NSString stringWithFormat:@"insert into 'text1' (ID,name,age) values(%d,'%@',%ld)",1,name,age];
}
因为ID为主键不能重复,所以该句代码插入时一定会失败,运行完打开数据库后会发现text1表里面一条数据也没有,这就说明了事务的原子性,一个成功则都成功,一个失败则都失败。
三.多线程
如果对数据的操作都在同一线程则上面的FMDatabase就可以满足需求,但是如果在不同线程下操作数据库,那么FMDatabase的缺陷就会体现出来了,接下来进行一下测试
在DataManager.h中添加如下方法
//多线程不使用FMDatabaseQueue
- (void)buildDatabaseNotWithQueue;
在DataManager.m中实现
- (void)buildDatabaseNotWithQueue{
NSString * docuPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString * path = [docuPath stringByAppendingPathComponent:@"student1.sqlite"];
NSLog(@"%@",path);
FMDatabase * db = [FMDatabase databaseWithPath:path];
dispatch_queue_t queuet1 = dispatch_queue_create("queuet1", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queuet1, ^{
NSLog(@"%@",[NSThread currentThread]);
for (int i = 0; i < 50; i ++) {
if ([db open]) {
BOOL result = [db executeUpdate:@"create table if not exists student (ID integer primary key autoincrement,name text not null,age integer not null);"];
if (result) {
NSString * insertSql1 = [NSString stringWithFormat:@"insert into student (ID,name,age) values (%d,'洋洋——%d',%d)",i+100,i,i];
BOOL res = [db executeUpdate:insertSql1];
if (!res) {
NSLog(@"1插入失败 ----%d",i);
}
}
}
}
});
dispatch_async(queuet1, ^{
NSLog(@"%@",[NSThread currentThread]);
for (int i = 0; i < 50;i ++) {
if ([db open]) {
BOOL result = [db executeUpdate:@"create table if not exists student (ID integer primary key autoincrement,name text not null,age integer not null);"];
if (result) {
NSString * insertSql2 = [NSString stringWithFormat:@"insert into student (ID,name,age) values (%d,'静静——%d',%d)",i,i,i];
BOOL res = [db executeUpdate:insertSql2];
if (!res) {
NSLog(@"2插入失败---%d",i);
}
}
}
}
});
}
在ViewController.m中调用
#pragma mark - 不使用多线程
- (void)buildDBNotWithFMDatabaseQueue{
[[DataManager shareDataManager] buildDatabaseNotWithQueue];
}
会发现打印台会打印如下数据
再打开数据库的student表
发现插入的数据远远不够100条,说明大多数数据都插入失败了。
所以说FMDatabase不能保证线程安全,这个时候就需要使用FMDatabaseQueue了。
同样在DataManager.h中添加方法
//多线程处理
- (void)buildDatabaseWithQueue;
在DataManager.m中实现
#pragma mark - 多线程保证线程安全FMDatabaseQueue
- (void)buildDatabaseWithQueue{
NSDate * begin = [NSDate date];
NSString * docuPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString * path = [docuPath stringByAppendingPathComponent:@"student.sqlite"];
NSLog(@"%@",path);
FMDatabaseQueue * queue = [FMDatabaseQueue databaseQueueWithPath:path];
dispatch_queue_t queuet1 = dispatch_queue_create("queuet1", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queuet1, ^{
NSLog(@"%@",[NSThread currentThread]);
for (int i = 0; i < 50; i ++) {
[queue inDatabase:^(FMDatabase * _Nonnull db) {
if ([db open]) {
BOOL result = [db executeUpdate:@"create table if not exists student (ID integer primary key autoincrement,name text not null,age inteter not null);"];
if (result) {
NSString * insertSql1 = [NSString stringWithFormat:@"insert into student (name,age) values ('洋洋_%d',25%d)",i,i];
BOOL res = [db executeUpdate:insertSql1];
if (!res) {
NSLog(@"插入失败 ------------%d",i);
}
}
}
}];
}
});
dispatch_group_async(group, queuet1, ^{
NSLog(@"%@",[NSThread currentThread]);
for (int i = 0; i < 50; i ++) {
[queue inDatabase:^(FMDatabase * _Nonnull db) {
if ([db open]) {
BOOL result = [db executeUpdate:@"create table if not exists student (ID integer primary key autoincrement,name text not null,age integer not null);"];
if (result) {
NSString * insertSql2 = [NSString stringWithFormat:@"insert into student (name,age) values ('静静_%d',25%d)",i,i];
BOOL res = [db executeUpdate:insertSql2];
if (!res) {
NSLog(@"2插入失败 ------------%d",i);
}
}
}
}];
}
});
dispatch_group_notify(group, queuet1, ^{
NSDate * end = [NSDate date];
NSTimeInterval time = [end timeIntervalSinceDate:begin];
NSLog(@"在多线程执行插入100条用时%f",time);
});
}
执行完成后查看数据库student表
注意这里并没有传入主键ID而是让其自动增长的,根据插入的先后顺序。
根据表格发现数据是交替的,说明两个线程同时开始进行,并且都将数据全部插入表格,
既然是两个线程同时进行,时间上应该也会小于单线程插入100条数据所花费的时间。
这里只是一个例子,同样可以将FMDatabaseQueue也参照FMDatabase封装出增,删,改,查等方法。
四:数据库加密
这里以FMDatabase为例,在不改动FMDatabase类的基础上,我们需要创建一个新的类起名为EncryptDatabase继承FMDatabase。
然后定义两个方法在.h文件中
+ (id)databaseWithPath:(NSString *)inPath encrytKey:(NSString *)encrytKey;
- (id)initWithPath:(NSString *)path encrytKey:(NSString *)encrytKey
每个方法都有一个参数,加密字符串。
在.m中实现该方法,并重写- (BOOL)open 和 - (BOOL)openWithFlags:(int)flags vfs:(NSString *)vfsName 方法。
.m文件中所有代码
#import "EncryptDatabase.h"
@interface EncryptDatabase()
{
NSString * _encryptKey;
}
@end
@implementation EncryptDatabase
+ (id)databaseWithPath:(NSString *)inPath encrytKey:(NSString *)encrytKey{
return [[[self class] alloc] initWithPath:inPath encrytKey:encrytKey];
}
- (id)initWithPath:(NSString *)path encrytKey:(NSString *)encrytKey{
if (self = [super initWithPath:path]) {
_encryptKey = encrytKey;
}
return self;
}
#pragma mark - 重写父类open方法
- (BOOL)open{
BOOL res = [super open];
if (res && _encryptKey) {
[self setKey:_encryptKey];
}
return res;
}
- (BOOL)openWithFlags:(int)flags vfs:(NSString *)vfsName{
BOOL res = [super openWithFlags:flags vfs:vfsName];
if (res && _encryptKey) {
[self setKey:_encryptKey];
}
return res;
}
@end
接下来使用代码调用调用,将EncryptDatabase.h导入到DataManager中,在DataManager.h中声明创建加密数据库的方法
//创建加密数据库
- (void)buildFMDatabaseEncrypt;
在DataManager.m中实现
- (void)buildFMDatabaseEncrypt{
NSString * docuPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString * path = [docuPath stringByAppendingPathComponent:@"encrypt_test.db"];
NSLog(@"%@",path);
EncryptDatabase * db = [EncryptDatabase databaseWithPath:path encrytKey:ENCRYPTKEY];
if ([db open]) {
BOOL result = [db executeUpdate:@"create table if not exists student (ID integer primary key autoincrement,name text not null,age integer not null);"];
if (result) {
for (int i = 0; i < 50; i ++) {
NSString * insertSql1 = [NSString stringWithFormat:@"insert into student (ID,name,age) values (%d,'当当%d',%d)",i,i,i];
BOOL res = [db executeUpdate:insertSql1];
if (res) {
NSLog(@"插入成功 --%d",i);
}else{
NSLog(@"插入失败 ---------%d",i);
}
}
}
}
FMResultSet * result = [db executeQuery:@"select * from 'student' where ID = 1"];
if (result) {
while ([result next]) {
NSString * name = [result stringForColumn:@"name"];
NSLog(@"%@",name);
}
}else{
NSLog(@"查询失败");
}
}
附带了一个查询语句
在ViewController.m中调用
#pragma mark - 创建加密数据库
- (void)buildEncryptDatabase{
[[DataManager shareDataManager] buildFMDatabaseEncrypt];
}
会发现可以查询成功并打印出name的值,接下里复制数据库地址然后打开会有这样的提示
说文件是被加密的,或者不是数据库。这就说明数据库加密成功。