iOS数据库升级

为什么要版本迁移

一个有明确产品定义的公司所做的软件一定不会是一个版本的,(本人😳过那种没有明确产品定义的公司,那真是一个月一个软件,做一个丢一个,更别提版本迭代了,这种公司很少见,不多说了)。一个正常的公司,项目开发中不得不考虑本版本迭代的问题,既然要版本迭代,一定会有改变数据表结构的情况,这时候就要考虑数据迁移了。
 当然了首先数据持久化是要会的哦,不太了解可以看我的iOS数据持久化

先说一下FMDB:

FMDB 介绍页面,推荐了 FMDBMigrationManager ,开源库。
所以这个就拿它说一下吧,其实用不用它也无所谓,(也可以自己写sql根据本地表做迁移工作)应用这个三方库更简化一些而已,自动管理版本,不必操作复杂的代码操作,只需要写好SQL语句就好。
 其实版本升级无非就是对表结构作更改而已,增加字段、增加表、删除表等等吧,只要表结构最终达到目标需要,无论怎么样写这个升级都可以,不必追求什么固定的方式:
FMDBMigrationManager为我们提供了两种方式:
第一种:
新建一个遵守<FMDBMigrating>协议的类:

#import <Foundation/Foundation.h>
#import "FMDB.h"
#import "FMDBMigrationManager.h"

@interface MigrationManager : NSObject <FMDBMigrating>

// 升级语句用数组的方式传入,可能有多个升级语句。
- (instancetype)initWithName:(NSString *)name andVersion:(uint64_t)version andExecuteUpdateArray:(NSArray *)updateArray;

- (BOOL)migrateDatabase:(FMDatabase *)database error:(out NSError *__autoreleasing *)error;

// 升级描述
@property (nonatomic, readonly) NSString *name;
// 版本号
@property (nonatomic, readonly) uint64_t version;

@end
#import "MigrationManager.h"

@interface MigrationManager ()

@property(nonatomic, copy)NSString *myName;

@property(nonatomic, assign)uint64_t myVersion;

@property(nonatomic, strong)NSArray *updateArray;

@end

@implementation MigrationManager

- (instancetype)initWithName:(NSString *)name andVersion:(uint64_t)version andExecuteUpdateArray:(NSArray *)updateArray {
    if (self = [super init]) {
        _myName = name;
        _myVersion = version;
        _updateArray = updateArray;
    }
    return self;
}

- (NSString *)name {
    return _myName;
}

- (uint64_t)version {
    return _myVersion;
}

- (BOOL)migrateDatabase:(FMDatabase *)database error:(out NSError *__autoreleasing *)error {
    for(NSString *updateStr in _updateArray) {
        [database executeUpdate:updateStr];
    }
    return YES;
}

@end

然后进行数据表做升级处理:

    static  FMDatabase *_db;
    static  NSString *_fileName;

    _fileName = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/data.sqlite"];
    NSLog(@"%@", _fileName);
    _db = [FMDatabase databaseWithPath:_fileName];
  
    if ([_db open]) {
        [_db executeUpdate:@"create table if not exists book(id integer primary key autoincrement, bookNumber integer not null, bookName text not null, authorID integer not null, pressName text not null);"];
    }
    
    [_db close];

    // FMDBMigrationManager 创建
    FMDBMigrationManager * manager = [FMDBMigrationManager managerWithDatabaseAtPath:_fileName migrationsBundle:[NSBundle mainBundle]];
    // sql语句添加到数组的形式,就是可以写多条。
    // 版本一
    MigrationManager * migration_1 = [[MigrationManager alloc]initWithName:@"新增USer表" andVersion:1 andExecuteUpdateArray:@[@"create table User(name text,age integer,sex text,phoneNum text)"]];
    [manager addMigration:migration_1];
    // 版本二
    MigrationManager * migration_2 = [[MigrationManager alloc]initWithName:@"USer表新增字段email" andVersion:2 andExecuteUpdateArray:@[@"alter table User add email text"]];
    [manager addMigration:migration_2];

    // 创建版本号表
    // 执行完该语句,再去我们的数据库中查看,会发现多了一个表 schema_migrations
    BOOL resultState = NO;
    NSError *error = nil;
    if (!manager.hasMigrationsTable) {
        resultState = [manager createMigrationsTable:&error];
    }
    // UINT64_MAX 表示升级到最高版本
    resultState = [manager migrateDatabaseToVersion:UINT64_MAX progress:nil error:&error];

第二种:
添加sql文件的方式:
这种方式代码量更少,但是随着每个版本的升级,sql文件会增多。

    static  FMDatabase *_db;
    static  NSString *_fileName;

    _fileName = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/data.sqlite"];
    NSLog(@"%@", _fileName);
    _db = [FMDatabase databaseWithPath:_fileName];
  
    if ([_db open]) {
        [_db executeUpdate:@"create table if not exists book(id integer primary key autoincrement, bookNumber integer not null, bookName text not null, authorID integer not null, pressName text not null);"];
    }
    
    [_db close];

    // [NSBundle mainBundle]是保存数据库升级文件的位置 根据自己放文件的位置定
    FMDBMigrationManager *manager = [FMDBMigrationManager managerWithDatabaseAtPath:_fileName migrationsBundle:[NSBundle mainBundle]];
    // 创建版本号表schema_migrations
    BOOL resultState = NO;
    NSError *error = nil;
    if (!manager.hasMigrationsTable) {
        resultState = [manager createMigrationsTable:&error];
    }
    // UINT64_MAX 表示升级到最高版本
    resultState = [manager migrateDatabaseToVersion:UINT64_MAX progress:nil error:&error];

   然后创建sql文件,以版本递增的方式,比如 1_EditionFileTable、2_EditionFileTable、3_EditionFileTable的方式规划版本,只需要在每一次升级的sql文件中写好sql语句就好,比如在1_EditionFileTable添加一个User表:

    CREATE TABLE User(
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    name TEXT,
    age integer
    );

如图所示:


如图.png

差不多就这些吧,是不是很简单,怎么灵活的写sql语句,这里就不说了,百度一下,有很多,大概改一改表名,字段名,很好应用。

接下来说一下CoreData:
轻量级迁移:

在使用Core Data的iOS App上,不同版本上的数据模型变更引发的数据迁移都是由Core Data来负责完成的。
 这种数据迁移模式称为Lightweight Migration(对于开发人员来说是lightweight-轻量级),开发人员只要在添加Persistent Store时设置好对应选项,其它的就交付给Core Data来做了:

NSDictionary *optionsDictionary = @{NSMigratePersistentStoresAutomaticallyOption:@YES,
                                    NSInferMappingModelAutomaticallyOption:@YES
                                   };

1. NSMigratePersistentStoresAutomaticallyOption:@YES,自动迁移Persistent Store,告诉 CoreData NSPersistentStoreCoordinator 如果存储层的 Model 和实际的 Model 不匹配的话(即修改了托管对象模型模板),CoreData会自动试着将旧版本的持久化存储区迁移到最新版的模型中。

2. NSInferMappingModelAutomaticallyOption:@YES,自动创建Mapping Model(映射模型或自动推测模型),为迁移Persistent Store服务的,所以当自动迁移Persistent Store选项设为YES、且找不到Mapping Model时,coordinator会尝试创建一份。
 作用是自动推断映射模型,当你选用新的模板后,CoreData会去自动推断原有的模型实体中的属性会对应于新模型实体中的哪一个属性(映射)。如果Value为NO,CoreData不会去自动推断,而新模板的实体对于旧模板的实体已经有改动了,但CoreData还是会默认新模板上的实体和旧模板的上的实体一一对应,结果映射不上,导致系统错误造成崩溃。

既然是尝试创建,便有成功和失败的不同结果。只有当数据模型的变更属于某些基本变化时,才能够成功地自动创建出推断出一份Mapping Model。

[注意]:在实体属性迁移时候,用该方式不靠谱,不一定能推断出来,很可能更新后直接闪退报错了,可能是因为表结构太复杂,超过了它简单推断的能力范围了,所以,在进行复杂的实体属性迁移到另一个属性迁移的时候,不要太相信这种方式,还是最好自己Mapping一次。当然,你要是新建一张表的时候,这2个参数是必须要加上的!!!

因为可能创建Mapping Model失败,所以考虑容错性的话,可以事先判断下能否成功推断出一份Mapping Model:

// 源数据模型sourceModel,destinationModel目标数据模型,NSManagedObjectModel
NSMappingModel *mappingModel = [NSMappingModel inferredMappingModelForSourceModel:sourceModel
                                                                 destinationModel:destinationModel
                                                                            error:outError];

如果无法创建一份Mapping Model,则会返回nil,并带有具体原因。

[注意]:CoreData在处理表结构更改的情况,需要一个新的表结构来替换旧的表结构,不能直接在原有的可视化托管对象模板模型(Model.xcdatamodeld)上修改会导致应用崩溃。因为在你修改完原有的模型模板结构后重新运行程序加载持续化存储区(store)时,系统会默认用新模板(修改过的模板)去打开原有的存储区,而原有的存储区是通过旧模板模型创建的,从而导致系统错误导致崩溃。

迁移步骤:
 1.选中Model.xcdatamodeld模型文件,点击上方菜单栏的Editor,在列表里选Add Model Version选项。

新建Model(一).png

并在弹出的对话框中有新模型模板名字和基于某个旧版本模型模板的选项,填写/选择后点击右下角Finish,新模型模板就创建好了。


新建Model(二).png

2.当新的模型模板创建成功后,会在Model.xcdatamodeld列表中会显示新的模型模板文件,选中新的模板就可以在里面根据新的需求修改实体、属性了。

3.为了让CoreData使用新的模板还需要修改当前模板版本,操作如下图:
 选中Model.xcdatamodeld,在模型文件的Inspector列表的下方找到Model Version选项,在Current列表中选择刚创建的模型文件。

4.如果建了Create NSManagedObject Subclass(NSManagedObject子类及分类),别忘记重新生成或者修改。

选择Model.png

代码示例:

// 返回 持久化存储协调者---persistentStoreCoordinator属性返回PSC对象
// 设置对象的存储方式和数据存放位置
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    if (_persistentStoreCoordinator) {
        return _persistentStoreCoordinator;
    }
    
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreData____.sqlite"];
    NSLog(@"sqlite:URL:%@", storeURL);
    //实例化PSC对象
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];//传入模型对象,初始化NSPersistentStoreCoordinator
    
    NSError *error = nil;
    NSString *failureReason = @"There was an error creating or loading the application's saved data.";
    
    NSDictionary *optionsDictionary = @{NSMigratePersistentStoresAutomaticallyOption:@YES,
                                        NSInferMappingModelAutomaticallyOption:@YES
                                        };
    //NSMigratePersistentStoresAutomaticallyOption   设为YES表示支持版本迁移
    //NSInferMappingModelAutomaticallyOption         设为YES表示支持版本迁移映射
      
    //为PSC对象添加新的持久化数据存储,其中NSSQLiteStoreType指数据持久化类型是SQLite数据
    [_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:optionsDictionary error:&error]

    return _persistentStoreCoordinator;
}
默认迁移:

如果你对新模型所做的修改并不被轻量级迁移所支持,那么你就需要创建一个映射模型。一个映射模型需要一个源数据模型和一个目标数据模型。
 新版本的某项数据是旧版本某项数据映射得到的,但实体名字不相同。
 首先禁用轻量级迁移开启的自动推断映射模型,这样能够确定手动创建的映射模型是不是在使用并能正常运行,NSInferMappingModelAutomaticallyOption:@NO。

迁移步骤:
 1.command+N创建MappingModel。

创建MappingModel.png

2.然后在弹出的对话框中选择旧版本的xcdatamodel文件作为Source Data Model点击Next。


第一步.png

3.再在新弹出的对话框中选择新版本的xcdatamodel文件作为Target Data Model并点击Next。


第二步.png

4.选择新生成的xcmappingmodel文件,在文件右侧选择Inspector列表里将Source改为旧版本的资源属性,修改后Mapping Name和Type会自动修改。


更改Source.png

5.在xcmappingmodel文件PeopleToStudent列表里选择你要映射的属性并将右侧的Attribute Mappings列表里的Value Expression修改成$source.xxx(xxx是旧版本的资源属性)。


映射.png

6.将最新的模型模板设置为最新版本的模型模板,运行程序,迁移就完成了。


更改.png
版本迁移过程分析:

首先,发生数据迁移需要三个基本条件:可以打开既有persistent store的sourceModel源数据模型,destinationModel目标数据模型,以及这两者之间的映射关系模型Mapping Model。
 利用这三样,当调用如下代码时:

_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if(![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:optionsDictionary error:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    }

Core Data创建了两个stack(分别为source stack和destination stack),然后遍历Mapping Model里每个entity的映射关系,做以下三件事情:

1. 基于source stack,Core Data先获取现有数据,然后在destination stack里创建当前entity的实例,只填充属性,不建立关系;
 2. 重新创建entity之间的关系;
 3. 验证数据的完整性和一致性,然后保存。

迁移管理器迁移:

首先检查一下该存储区存不存在,再比较原有的存储模型是否与现在的存储模型相同,如果不相同那么就需要我们进行数据迁移了。

// 检查一下该存储区存不存在,再把存储区里面的model metadata进行比较,判断是否需要进行数据迁移。
- (BOOL)isMigrationNecessaryForStore:(NSURL *)storeURL {
    //是否存在文件,如果不存在直接返回NO
    if (![[NSFileManager defaultManager] fileExistsAtPath:storeURL.path]) {
        NSLog(@"跳过迁移:源数据库丢失");
        return NO;
    }
    
    NSError *error = nil;
    //比较存储模型的源数据。
    NSDictionary *sourceMataData = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
                                                                                              URL:storeURL
                                                                                            error:&error];
    //源数据模型
    //NSManagedObjectModel *sourceModel = [NSManagedObjectModel mergedModelFromBundles:nil
    //                                                                forStoreMetadata:sourceMataData];
    //目标数据模型
    NSManagedObjectModel *destinationModel = [self persistentStoreCoordinator].managedObjectModel;
    
    if ([destinationModel isConfiguration:nil compatibleWithStoreMetadata:sourceMataData]) {
        NSLog(@"跳过迁移:源已经兼容");
        return NO;
    }
    
    return YES;
}

当上面函数返回YES,我们就需要迁移了。

- (BOOL)migrateStore:(NSURL *)sourceStore {
    //NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
    
    BOOL successs = NO;
    NSError *error = nil;
    
    //原来的数据模型的原信息
    NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
                                                                                              URL:sourceStore
                                                                                            error:&error];
    //原数据模型
    NSManagedObjectModel *sourceModel = [NSManagedObjectModel mergedModelFromBundles:nil
                                                                    forStoreMetadata:sourceMetadata];
    
    //最新版数据模型
    NSManagedObjectModel *destinModel = [self managedObjectModel];
    
    //数据迁移的映射模型
    NSMappingModel *mappingModel = [NSMappingModel mappingModelFromBundles:nil
                                                            forSourceModel:sourceModel
                                                          destinationModel:destinModel];
    
    if (mappingModel) {
        NSError *error = nil;
        
        //迁移管理器
        NSMigrationManager *migrationManager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel
                                                                              destinationModel:destinModel];
        
        //注册监听 NSMigrationManager 的 migrationProgress 来查看进度
        [migrationManager addObserver:self
                           forKeyPath:@"migrationProgress"
                              options:NSKeyValueObservingOptionNew
                              context:NULL];
        
        //先把模型存储到Temp.sqlite
        NSURL *destinStore = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreData____.sqlite"];
        //管理迁移库从储存URL到目的URL->真正发生数据迁移
        successs = [migrationManager migrateStoreFromURL:sourceStore
                                                    type:NSSQLiteStoreType
                                                 options:nil
                                        withMappingModel:mappingModel
                                        toDestinationURL:destinStore
                                         destinationType:NSSQLiteStoreType
                                      destinationOptions:nil
                                                   error:&error];
        if (successs) {
            //成功后替换掉原来的旧的文件
            if ([self replaceStore:sourceStore withStore:destinStore]) {
                //这里移除监听就可以了。
                NSLog(@"成功地迁移到当前模型:%@", sourceStore.path);
                [migrationManager removeObserver:self forKeyPath:@"migrationProgress"];
            }else
            {
                NSLog(@"失败的迁移: %@",error);
            }
            
        }else
        {
            NSLog(@"迁移失败:映射模型为空");
        }
    }
    
    return successs;
}

文件替换

- (BOOL)replaceStore:(NSURL *)old withStore:(NSURL *)new {
    BOOL success = NO;
    NSError *error = nil;
    if ([[NSFileManager defaultManager] removeItemAtURL:old error:&error]) {
        error = nil;
        if ([[NSFileManager defaultManager] moveItemAtURL:new toURL:old error:&error]) {
            success = YES;
        }
    }
    return success;
}

如果迁移进度有变化,会通过观察者,observeValueForKeyPath来告诉用户进度,这里可以监听该进度,如果没有完成,可以来禁止用户执行某些操作

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    
    if ([keyPath isEqualToString:@"migrationProgress"]) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            
            float progress = [[change objectForKey:NSKeyValueChangeNewKey] floatValue];
            
            int percentage = progress * 100;
            NSString *string = [NSString stringWithFormat:@"Migration Progress: %i%%", percentage];
            NSLog(@"进度:%@",string);
        });
    }
}

[注]:一般打开app沙盒里面的会有三种类型的文件,sqlite,sqlite-shm,sqlite-wal,后面2者是iOS7之后系统会默认开启一个新的“数据库日志记录模式”(database journaling mode)生成的,sqlite-shm是共享内存(Shared Memory)文件,该文件里面会包含一份sqlite-wal文件的索引,系统会自动生成shm文件,所以删除它,下次运行还会生成。sqlite-wal是预写式日志(Write-Ahead Log)文件,这个文件里面会包含尚未提交的数据库事务,所以看见有这个文件了,就代表数据库里面还有还没有处理完的事务需要提交,所以说如果有sqlite-wal文件,再去打开sqlite文件,很可能最近一次数据库操作还没有执行。

所以在调试的时候,我们需要即时的观察数据库的变化,我们就可以先禁用这个日志记录模式,只需要在建立持久化存储区的时候存入一个参数即可。具体代码如下:

NSDictionary *optionsDictionary = @{
                                   NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"}
                                   };
        
NSPersistentStore *store = [_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
                                                                     configuration:nil
                                                                               URL:storeURL
                                                                           options:optionsDictionary
                                                                             error:&error];
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容