iOS数据存储(中)

上一篇文章总结的主要是一些基础的数据存储,下面来聊一聊关系型数据库的存储,其实在iOS上主要就是用SQLite或者Core Data。其实两者有很多共通之处,也存在许多不同,各有自己擅长的领域。

先来聊一聊SQLite吧,众所周知,SQLite是一个关系型数据库,为嵌入式设备量身定制,具有小而强大的特点,目前iOS系统中所使用的SQLite默认使用WAL模式,英文全称Write Ahead Logging,相比之前的模式,其提高了读写并发,并大大加快写入数据的速度。

其实现原理大致是这样的,我们在写数据时,并不是直接操作数据库文件,而是操作一个WAL文件,这样当我们插入、或者删除数据时,只是写一条操作语句到WAL文件,当WAL文件达到一定数量级(默认是1000page)时,sqlite就会把数据同步到数据库文件中。其带来的不足之处是读数据时,可能要同时考虑到WAL文件与数据库文件,所以读数据性能会有一定下降。但这个对于移动端的数据量来说表现得并不明显。

所以数据库在删除数据后,数据库文件大小有时候反而会增大。但并不会无限制地增大,当增长到一定数量时,其就会变小。


基本操作

通常,我们在iOS应用中我们会使用FMDB来操作SQLite.

创建数据库表

    FMDatabase * db = [FMDatabase databaseWithPath:self.dbPath];
    if ([db open]) {
        [db executeUpdate:@"create table easy (a text)"];
        [db close];
    } else {
        debugLog(@"error when open db");
    }

数据库写操作

    FMDatabase * db = [FMDatabase databaseWithPath:self.dbPath];
    if ([db open]) {
        NSString * sql = @"insert into user (name, password) values(?, ?) ";
        BOOL res = [db executeUpdate:sql, name, @"boy"];
        if (!res) {
            debugLog(@"error to insert data");
        } else {
            debugLog(@"succ to insert data");
        }
        [db close];
    }

数据库读操作

    FMDatabase * db = [FMDatabase databaseWithPath:self.dbPath];
    if ([db open]) {
        NSString * sql = @"select * from user";
        FMResultSet * rs = [db executeQuery:sql];
        while ([rs next]) {
            int userId = [rs intForColumn:@"id"];
            NSString * name = [rs stringForColumn:@"name"];
        }
        [db close];
    }

SQLite的很多其它语法可以在网站上面查看和学习。在使用SQLite的过程中,需要注意下面几个问题。


注意事项

  • 数据迁移

试想我们已经在APP里面创建了一张表,但我们后面的需求是要在这边表上加一个字段,如果只是简单地在create table后面添加一个字段,其实是无法奏效的。我们需要用到关键字ALTER.

ALTER TABLE table_name ADD colume_name column_type;

如果我们的需求是要废弃某个字段,那操作起来要麻烦一些,因为SQLite并不支持直接DROP COLUMN的操作,网上有一个变通的解决方案:

BEGIN TRANSACTION;  
CREATE TEMPORARY TABLE test_backup(a,b);  
INSERT INTO test_backup SELECT a,b FROM test;  
DROP TABLE test;  
CREATE TABLE test(a,b);  
INSERT INTO test SELECT a,b FROM test_backup;  
DROP TABLE test_backup;  
COMMIT;  

从上面我们可以看出,废弃一个字段的成本是很高的,尤其当我们数据库的内容很多的时候,这样迁移数据,就不得不得考虑手机内存是否够用。

所以我们通常的作法是不理采,其实我们还有一些做数据迁移手法,例如使用存储类型BLOB,我们可以理解其为一个NSData类型的数据,我们通过序列化,把一些额外的字段通通塞进这种数据类型里面。

不过这样一来,我们就需要额外序列化与反序列化,势必增加一些性能开销。


  • 多线程问题

操作SQLite通常会涉及到I/O处理,如果全在主线程完成,可能会阻塞主线程,于是我们通常需要使用多线程来解决这一问题。

SQLite本身也提供了几种解决方案来处理这个问题。但我们在iOS系统中所使用的SQLite本身并不是线程安全的,不过好在FMDB解决了这个问题。其使用一个串行队列FMDatabaseQueue来实现线程之间的同步。

    self.queue = [FMDatabaseQueue databaseQueueWithPath:self.databasePath];
dispatch_async(q1, ^{
    [self.queue inDatabase:^(FMDatabase *adb) {
        int count = 0;
        FMResultSet *rsl = [adb executeQuery:@"select * from qfoo where foo like 'h%'"];
        while ([rsl next]) {
            count++;
        }

        count = 0;
        rsl = [adb executeQuery:@"select * from qfoo where foo like ?", @"h%"];
        while ([rsl next]) {
            count++;
        }
    }];

如果是只读操作的话,还可以使用

FMDatabaseQueue *queue2 = [FMDatabaseQueue databaseQueueWithPath:self.databasePath flags:SQLITE_OPEN_READONLY];

  • 性能问题

上面谈到可以用可以用executeQuery:执行block来处理任务,但如果想要获得更高的性能表现,通常用到的手法之一就是使用Transaction事务

self.pool = [FMDatabasePool databasePoolWithPath:self.databasePath];
[self.pool inTransaction:^(FMDatabase *adb, BOOL *rollback) {
        [adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1001]];
        [adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1002]];
        [adb executeUpdate:@"insert into easy values (?)", [NSNumber numberWithInt:1003]];
 }];

当然上面谈到的多线程问题,也同样可以使用事务。

另外一个提高性能的手段,就那是就创建索引,索引可以大大加快查询的速度,但这里要注意的是,并不是所有的查询语句都可以利用到索引技术。如果查看有没有使用到索引,可以通过命令EXPLAIN QUERY PLAN。这个在SQLite菜鸟教程里面有介绍。
创建索引的语法如下,当然要移除索引,调用DROP关键字就可以了。

CREATE INDEX if not exists archivedSortDateIndex on notes (archived, sortDate);

索引其实是加快了读的速度,但写数据的速度同时也会有所下降,在使用前必须衡量好读写之间的平衡。


  • 数据库加密

iOS8之后就不能通过iTool等工具直接访问App沙盒中的Document目录了,数据库放到这些目录下事实上安全系数已经有了初步的保证,但如果数据过于敏感,需要加密的话,也是可以对数据库加密的,通用的加密方式有两种。

1、能数据内容进行加密(通常是AES等对称加密算法),把数据存储为blob类型。
2、另外一个方式就是对数据库进行整体加密,目前比较能用的作法是使用SQLCipher。幸运的是其可以与FMDB直接结合,详细的作法可以参考用SQLCipher加密数据库。我试了一下,的确可以有效地加密数据。


  • 其它

Distinct关键字可以用来做数据去重处理,结合上NSMutableSet,可以有一个不错的发挥,另外Join可以处理现个不不同表之间的数据。


总结

SQLite是一个相当成熟的技术,在移动端领域有着广泛的使用,尤其是FMDB与中文分词、建立索引的处理,与iOS9的Spotlight技术相结合,可以做的事情还有很多。
而其最大的特点,我个人觉得是灵活、高效。而且学习成本低,因为大家或多或少接触过SQL。

2015-11-01 10:43370