为什么要版本迁移
一个有明确产品定义的公司所做的软件一定不会是一个版本的,(本人😳过那种没有明确产品定义的公司,那真是一个月一个软件,做一个丢一个,更别提版本迭代了,这种公司很少见,不多说了)。一个正常的公司,项目开发中不得不考虑本版本迭代的问题,既然要版本迭代,一定会有改变数据表结构的情况,这时候就要考虑数据迁移了。
当然了首先数据持久化是要会的哦,不太了解可以看我的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
);
如图所示:
差不多就这些吧,是不是很简单,怎么灵活的写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选项。
并在弹出的对话框中有新模型模板名字和基于某个旧版本模型模板的选项,填写/选择后点击右下角Finish,新模型模板就创建好了。
2.当新的模型模板创建成功后,会在Model.xcdatamodeld列表中会显示新的模型模板文件,选中新的模板就可以在里面根据新的需求修改实体、属性了。
3.为了让CoreData使用新的模板还需要修改当前模板版本,操作如下图:
选中Model.xcdatamodeld,在模型文件的Inspector列表的下方找到Model Version选项,在Current列表中选择刚创建的模型文件。
4.如果建了Create NSManagedObject Subclass(NSManagedObject子类及分类),别忘记重新生成或者修改。
代码示例:
// 返回 持久化存储协调者---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。
2.然后在弹出的对话框中选择旧版本的xcdatamodel文件作为Source Data Model点击Next。
3.再在新弹出的对话框中选择新版本的xcdatamodel文件作为Target Data Model并点击Next。
4.选择新生成的xcmappingmodel文件,在文件右侧选择Inspector列表里将Source改为旧版本的资源属性,修改后Mapping Name和Type会自动修改。
5.在xcmappingmodel文件PeopleToStudent列表里选择你要映射的属性并将右侧的Attribute Mappings列表里的Value Expression修改成$source.xxx(xxx是旧版本的资源属性)。
6.将最新的模型模板设置为最新版本的模型模板,运行程序,迁移就完成了。
版本迁移过程分析:
首先,发生数据迁移需要三个基本条件:可以打开既有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];