美文网首页
【重读iOS】数据持久化1:数据库框架

【重读iOS】数据持久化1:数据库框架

作者: FindCrt | 来源:发表于2018-09-17 17:39 被阅读26次

    Realm

    创建数据库

    使用RLMRealm *realm = [RLMRealm defaultRealm];默认的数据库配置,或者使用+ (nullable instancetype)realmWithConfiguration:(RLMRealmConfiguration *)configuration error:(NSError **)error;进行全方位的配置。

    调用了方法后数据库创建完成。

    创建表

    创建表也是在RLMRealm构建的方法里完成的,牛逼的地方在于:它使用objc_copyClassList把所有注册的类给拿到,然后把是从RLMObject继承的类提取出来,把这些类生成对应的表。

    所以对于使用者而言,只需要定义数据模型,并且这些模型类从RLMObject继承。

    • 属性就跟普通类一样定义,只是把属性的描述关键词去掉。大概是因为属性的getter/setter全部被重写了,这些属性都没用了。

    Realm ignores Objective‑C property attributes like nonatomic, atomic, strong, copy, weak, etc. These aren’t meaningful for Realm storage; it has its own optimized storage semantics.

    • 一对多属性
    //这个协议不知道什么用
    RLM_ARRAY_TYPE(Book) 
    //前一个尖括号是泛型,即数组的元素类型,后一个是协议
    @property (nonatomic) RLMArray<Book*><Book> *books; 
    
    • 反向关系

    图书馆(Library)里有书,属性books,书(Book)可以属于图书馆,属性owner。如果一本书加到一个新的图书馆里,那么是修改了Library的books,但是Book的owner不会发生改变。也就是两个属性有相互影响,让其中一个依赖另一个,这样维护一个属性的修改就可以了。

    owner跟随books修改:

    //类Book
    @property (readonly) RLMLinkingObjects *owners;
    +(NSDictionary<NSString *,RLMPropertyDescriptor *> *)linkingObjectsProperties{
      return @{
               @"owners" : [RLMPropertyDescriptor descriptorWithClass:Library.class propertyName:@"books"]
               };
    }
    
    • 还可以给RLMObject设置主键primaryKey,默认值defaultPropertyValues,忽略的属性ignoredProperties,必要属性requiredProperties,索引indexedProperties。比较有用的是主键和索引。

    数据操作

    Library *library = [[Library alloc] init];
    [realm transactionWithBlock:^{
        [realm addObject:library];
    }];
    

    构建和复制跟普通对象一样,存入数据库的时候使用事务。添加后对象就由realm管理了,对它属性的修改必须在写事务内操作,否则奔溃。

    Terminating app due to uncaught exception 'RLMException', reason: 'Attempting to modify object outside of a write transaction - call beginWriteTransaction on an RLMRealm instance first.'

    删改
    [realm transactionWithBlock:^{
         bk.name = @"和谐世界2";
         [realm deleteObject:bk];
    }];
    
    查询
    //查询全部
    [Book allObjects]
    //条件查询
    RLMResults<Book *> *results = [Book objectsWhere:@"age == 101"];
    

    where之后的字符串是用来构建NSPredicate的,所以按照它的语法来写。

    自动更新

    两个对象是对应着数据库里同一个数据,那么其中一个对象修改了,提交给数据库,另外一个对象也会自动跟随修改。

    RLMResults *results = [Book objectsWhere:@"age == 119"];
       Book *bk = results.firstObject;
       NSLog(@"1: %@",bk.name);
       
       RLMResults *results2 = [Book objectsWhere:@"age == 119"];
       Book *bk2 = results2.firstObject;
       NSLog(@"2: %@",bk2.name);
       
       [realm transactionWithBlock:^{
           bk.name = [NSString stringWithFormat:@"%@_修改+1",bk.name];
       }];
       NSLog(@"3: %@",bk2.name);
    

    第三次输出的名称就是修改后的名称了。

    查询结果也可以自动更新

    但是这些更新都限于当前线程,realm不支持跨线程的数据共享,新的线程需要新的RLMRealm对象且从新读取新的数据对象。

    数据迁移

    RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
        config.schemaVersion = 2;
        
        config.migrationBlock = ^(RLMMigration * _Nonnull migration, uint64_t oldSchemaVersion) {
            //数据迁移代码
            if (oldSchemaVersion < 1) {
                [migration enumerateObjects:Book.className block:^(RLMObject * _Nullable oldObject, RLMObject * _Nullable newObject) {
                    
                }];
            }
        };
        [RLMRealmConfiguration setDefaultConfiguration:config];
    

    realm的数据迁移逻辑是:

    • 判断config.schemaVersion的版本是否和数据库相同,不同且大于0执行数据迁移操作
    • 根据当前的类构建一个新的realm(realm的表是由类自动生成的,这一点优势也在这体现出来了),然后执行config.migrationBlock给新的realm填充数据
    • 如果migrationBlock啥也不干,其实新的表也会建起来,只是之前的数据丢失了。所以这里可以理解为两步:1.realm自动完成新的表的构建 2.我们在migrationBlock里完成对新表数据的填充
    • migrationBlock里面有旧的版本号,这样可以一步步的升级上来。简单说就是,有了新版本,加入这一次的迁移代码,下一次的时候不要删除前面的代码,这样就可以逐步更新了,如:
    config.migrationBlock = ^(RLMMigration * _Nonnull migration, uint64_t oldSchemaVersion) {
           //数据迁移代码
           if (oldSchemaVersion < 1) {
               //从0更新到1的操作
           }
           if (oldSchemaVersion < 2) {
               //从1更新到2的操作
           }
           if (oldSchemaVersion < 3) {
               //从2更新到3的操作
           }
       };
    

    如果直接从版本0、1、2直接更新到3,也可以把第三步放到前面,看具体需求。

    最后realm并不是基于sqlite的,是另写的数据库。

    FMDB

    FMDB只是针对sqlite做的轻量级的封装,没有模型和表的映射、没有对数据的监控、也没有数据迁移的帮助等等ORM的特性,只是把原本需要执行sql的操作做了一个函数封装。

    建库

    NSString *dbPath = [NSHomeDirectory() stringByAppendingString:@"/Documents/book.db"];
    FMDatabase *database = [FMDatabase databaseWithPath:dbPath];
    

    构建一个FMDatabase对象即可。
    不过使用之间要打开数据库,建立数据库连接:[database open]

    建表

    很普通的执行sql语句:

    BOOL state = [database executeStatements:
    @"create table if not exists Book (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)"];
    if (!state) {
        NSLog(@"create table error!");
        return;
    }
    

    插入数据和更新数据

    Book *bk = [[Book alloc] init];
    bk.name = @"Big World";
    bk.id = 123;
    bk.age = 10000;
        
    NSDictionary *bkKeyValues = @{
                                  @"id":@(bk.id),
                                  @"name":bk.name,
                                  @"age":@(bk.age)
                                  };
    [database executeUpdate:
    @"insert into Book values(:id, :name, :age)" withParameterDictionary:bkKeyValues];
    

    核心方法是executeUpdate:,这个函数是建立在sqlite3_prepare_v2上的。

    新增或者更新数据的时候,可以直接在sql语句内嵌入内容:
    insert table Book (name) values('sqlite权威指南')
    但如果数据比较长,则在内容部分填入占位字符,表示实际值后面再绑定:

    • sql语句: "insert into Book values(:name, :age)"
    • 使用sqlite3_prepare_v2执行语句
    • 再用sqlite3_bind_xxx系列的函数绑定实际的数据。比如name字段是字符串,使用sqlite3_bind_text(stmt,1,"sqlite权威指南"),这个索引是从1开始算的。

    而占位字符有几种格式:?,?number,:string,@string,$string,单纯的问号就是占位,它的索引是自动分配的,第二种number就是指定了索引,后面几种可以通过sqlite3_bind_parameter_index这个函数来查找对应的索引,传入的内容就是:string后面字符的内容。

    在FMDB里,如果你更新使用字典来传入数据,就是使用:string这种占位符,通过string内容:1. 从sqlite这边得到索引 2.从字典里拿到字段对应数据 ,把1和2的内容关联起来,使用sqlite3_bind_xxx函数传入。

    如果使用数组或者变参的方式传入数据,那么索引和数组里的数据意义对应:

    NSArray *infos = @[bk.name, @(bk.age)];
        [database executeUpdate:@"insert into Book (name, age) values(?,?)" withArgumentsInArray:infos];
    

    这里占位可以使用最简单的问号?,那么第一个占位就使用数组里第一个数据,第二个占位就使用第二个数据,依次类推。变参方式传入就是对应第一个参数,第二个参数......

    也就是说,这个函数的核心是如何处理绑定参数索引和实际值之间的对应关系,理解了这个问题,这个函数就理解了。

    更新数据跟插入数据逻辑一致,也是使用这个方法。

    查询数据

    查询时的输入逻辑和上面一样,使用sqlite3_prepare_v2处理sql语句,使用占位字符sqlite3_bind_xxx来传入数据。查询时wherelimitoffsetorder by这些的值都可以这么处理。

    插入和更新数据时只要执行完操作就可以了,而查询执行完executeQuery函数后,得到的只是FMResultSet对象,还要把数据提取出来:

    NSDictionary *queryInfos = @{@"name":@"insert array", @"limitx":@(2), @"order":@"id desc", @"ment":@"desc"};
    FMResultSet *result = [database executeQuery:@"select  name, id from Book where name = :name order by :order limit :limitx " withParameterDictionary:queryInfos];
    

    提取数据:

    NSMutableArray *models = [[NSMutableArray alloc] init];
    while ([result next]) {
        Book *book = [[Book alloc] init];
        book.id = [result longLongIntForColumnIndex:1];
        book.name = [result stringForColumnIndex:0];
    //        book.age = [result longLongIntForColumnIndex:0];
        
        [models addObject:book];
    }
    

    不断使用next函数调到下一条数据,内部核心是sqlite3_step。然后使用longLongIntForColumnIndex等一系列方法把属性值一个个的提取出来,赋值到对象上。

    这里便是原生的sqlite处理中最痛苦的一个环节,对于"一条数据-->一个对象"的转变需要一个字段一个字段的去处理。这样:

    • 字段多写的很痛苦,都是枯燥的代码
    • 对于每个表/模型都需要写一套代码,工作量大,而且一旦模型变动这个代码就要改。

    这时就需要ORM来拯救世界了,可惜CoreData除了这个工作之外还有很多的功能,甚至把一些细节都封闭了,导致用起来反而挺麻烦的。

    ORM, Object Relational Mapping的简写,从这里的工作里就可以很好的理解这个东西的意思。对象关系映射,把一种对象映射到另一种对象,这里工作的根本就是把数据库里的一条数据(数据库对象)自动的转为我们定义的模型对象。

    多线程环境

    使用FMDatabaseQueue来调用:

    FMDatabaseQueue *dbQueue = [[FMDatabaseQueue alloc] initWithPath:dbPath];
    [dbQueue inDatabase:^(FMDatabase * _Nonnull db) {
        [db executeUpdate:@"insert into Book values(200, 'dbQueueInsert', 2013)"];
    }];
    

    这是一个保守的方案:

    dispatch_sync(_queue, ^() {
      FMDatabase *db = [self database];
       block(db);
       ...
    }
    ...
    _queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
    

    这个_queue是一个串行的队列,所以使用FMDatabaseQueue来访问数据库,不管你在哪个线程调用,最后都到这个里_queue里处理,而它又是串行的,所以不会出现并发的情况。没有并发,就没有多线程的各种问题了。

    如果你采用这种方案,那么就有建一个全局的FMDatabaseQueue,在哪都用它,不同的queue之间是互发限制的,还是会并发,还是会触发问题。

    再者,因为_queue是串行的,而这里又使用了同步的方法dispatch_sync,所以嵌套调用会导致死锁。FMDB做了队列的检测,在同一个队列里调用inDatabase会crash。

    //使用dispatch_get_specific获取队列标识
    FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
    assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock");
    

    说这个是保守方案是因为,它完全的隔绝了多线程访问的这种操作,每个操作都依次进行,不并发。但实际至少读和读之间是可以共存的。

    其他库

    key-value数据库YTKKeyValueStore

    微信开源数据库wcdb

    尝试

    sqlite有挺多的特性可以支持ORM的实现,所以参照realm的一些思路,如runtime加载创建表,在FMDB的基础上实现了一个简单的ORM:可以自动建表,脱离sql进行增删改查。考虑到微信开源数据库wcdb和realm都已经是很成熟的方案了,我写了也没多大用,所以没有做太多的完善,只是算作一个尝试,让自己熟悉一下ORM的想法和对sqlite的熟悉。有兴趣的可以看一下TFDatabaseMapper

    相关文章

      网友评论

          本文标题:【重读iOS】数据持久化1:数据库框架

          本文链接:https://www.haomeiwen.com/subject/pbmrnftx.html