访问 ruicb.com,一键抵达我的博客!
在涉及数据库的应用中,我们不可能在应用刚上线时,就提前预知未来需要的字段,只能在后期根据新的需求去不断完善。所以,数据库的更新就显得十分重要,因为从最初搭建数据库,你就需要做好后期升级的机制。如果刚开始没有做,等 App 上线了,再想更新数据库以新增表或字段,你会发现是个大问题。
本文以使用 GreenDao 3.2 为例,侧重分享更新方案,至于基本配置及使用,网上已经有跟多优秀的译文或者博客,就不再赘述。
更新这块重视的人还不多,所以想记录一下,和大家一起交流。
GreenDao 3.2 中自带的更新类
,是不可用的,如下:
- DevOpenHelper
- /** WARNING: Drops all table on Upgrade! Use only during development. */public staticclassDevOpenHelper extends OpenHelper {
- public DevOpenHelper(Context context,Stringname) {
- super(context, name);
- }
- public DevOpenHelper(Context context,Stringname, CursorFactory factory) {
- super(context, name, factory);
- }
- @Override
- publicvoidonUpgrade(Database db, int oldVersion, int newVersion) {
- dropAllTables(db,true);
- onCreate(db);
- }
- }
注释明确说明了仅限于开发阶段,从上述代码可以看出,GreenDao 在数据库版本更新时,默认删除所有表再新建,开发阶段无所谓,但这对于线上 App 升级是致命的,这样一来老用户的数据就全丢了,所以不适合用于 App 上线后更新数据库。
可能我们会想,那我们改掉它不就行了吗?改是不行的,因为
是
- DevOpenHelper
的内部类,而
- DaoMaster
是 GreenDao 自动生成的,会在
- Daomaster
项目时被覆盖重写。
- build
看来只能自己写了,撸起袖子就是干。
仿照
,我们自定义
- DevOpenHelper
继承自
- MyOpenHelper
,并重写
- OpenHelper
方法以自己维护更新:
- onUpgrade()
- publicclassMyOpenHelper extends DaoMaster.OpenHelper {
- public MyOpenHelper(Context context,Stringname) {
- super(context, name);
- }
- public MyOpenHelper(Context context,Stringname, SQLiteDatabase.CursorFactory factory) {
- super(context, name, factory);
- }
- @Override
- publicvoidonUpgrade(Database db, int oldVersion, int newVersion) {
- super.onUpgrade(db, oldVersion, newVersion);//写自己的更新逻辑}
- }
此时,就可以在上述注释的位置写自己的逻辑了。
光写不行,要让 GreenDao 知道我们使用自定义的更新类,所以在初始化 GreenDao 的地方指明使用
,如下:
- MyOpenHelper
- publicclassApp extends Application {
- private static DaoSession daoSession;
- @Override
- publicvoidonCreate() {
- super.onCreate();//使用自定义更新类MyOpenHelper helper =newMyOpenHelper(this,"db-name");
- Database db = helper.getWritableDb();
- daoSession =newDaoMaster(db).newSession();
- }//对外暴露会话对象 DaoSessionpublic static DaoSession getDaoSession() {returndaoSession;
- }
- }
关联自定义的更新类之后,下面开始真正的更新逻辑,分别以
和
- 新增表
为例。
- 更新已有表的字段
随着项目迭代,假设这一版我们需要新增一个数据表,用来保存用户缓存的视频路径,大致步骤如下:
1. 新建
对象,用
- VideoCache
标识一下,加几个属性,再
- @Entry
一下项目,GreenDao 会自动帮我们补全
- build
和
- getter
方法,同时生成对应的
- setter
; 2. 修改
- VideoCacheDao
下
- app
中声明的数据库版本号,+1; 3. 在
- build.gradle
的
- MyOpenHelper
方法中创建新表:
- onUpgrade()
- @Override
- publicvoidonUpgrade(Database db, int oldVersion, int newVersion) {
- super.onUpgrade(db, oldVersion, newVersion);//这么写能更新,但实际还存在跨版本升级问题VideoCacheDao.createTable(db,false);
- }
4. 运行即可;
同样,随着版本迭代,以前的数据库表需要新增字段以满足现有的需求,以在
表中新增
- VideoCache
字段为例。新增字段不同于新增表,更新过程概括来说分为三步: 1. 备份
- FileSize
表到临时表
- VideoCache
; 2. 删除原来的
- VideoCache_Temp
表; 3. 新建带有
- VideoCache
字段的
- FileSize
表; 4. 迁移
- VideoCache
表的数据至新建的
- VideoCache_Temp
;
- VideoCache
上述过程自己实现起来还是有难度的,好在网上有开源的辅助类,直接拿过来,加点注释,如下:
- publicclassMigrationHelper {
- private static finalStringCONVERSION_CLASS_NOT_FOUND_EXCEPTION ="MIGRATION HELPER - CLASS DOESN'T MATCH WITH THE CURRENT PARAMETERS";
- private static MigrationHelper instance;
- public static MigrationHelper getInstance() {if(instance ==null) {
- instance =newMigrationHelper();
- }returninstance;
- }
- publicvoidmigrate(Database db, Class>... daoClasses) {//1. 备份表generateTempTables(db, daoClasses);//2. 删除所有表DaoMaster.dropAllTables(db,true);//3. 重新创建所有表DaoMaster.createAllTables(db,false);//4. 恢复数据restoreData(db, daoClasses);
- }/**
- * 备份要更新的表
- */privatevoidgenerateTempTables(Database db, Class>... daoClasses) {for(int i =0; i < daoClasses.length; i++) {
- DaoConfig daoConfig =newDaoConfig(db, daoClasses[i]);Stringdivider ="";StringtableName = daoConfig.tablename;StringtempTableName = daoConfig.tablename.concat("_TEMP");
- ArrayList<String> properties =newArrayList<>();
- StringBuilder createTableStringBuilder =newStringBuilder();
- createTableStringBuilder.append("CREATE TABLE ").append(tempTableName).append(" (");for(int j =0; j < daoConfig.properties.length; j++) {StringcolumnName = daoConfig.properties[j].columnName;if(getColumns(db, tableName).contains(columnName)) {
- properties.add(columnName);Stringtype =null;try{
- type = getTypeByClass(daoConfig.properties[j].type);
- }catch(Exception exception) {
- }
- createTableStringBuilder.append(divider).append(columnName).append(" ").append(type);if(daoConfig.properties[j].primaryKey) {
- createTableStringBuilder.append(" PRIMARY KEY");
- }
- divider =",";
- }
- }
- createTableStringBuilder.append(");");
- db.execSQL(createTableStringBuilder.toString());
- StringBuilder insertTableStringBuilder =newStringBuilder();
- insertTableStringBuilder.append("INSERT INTO ").append(tempTableName).append(" (");
- insertTableStringBuilder.append(TextUtils.join(",", properties));
- insertTableStringBuilder.append(") SELECT ");
- insertTableStringBuilder.append(TextUtils.join(",", properties));
- insertTableStringBuilder.append(" FROM ").append(tableName).append(";");
- db.execSQL(insertTableStringBuilder.toString());
- }
- }/**
- * 恢复数据
- */privatevoidrestoreData(Database db, Class>... daoClasses) {for(int i =0; i < daoClasses.length; i++) {
- DaoConfig daoConfig =newDaoConfig(db, daoClasses[i]);StringtableName = daoConfig.tablename;StringtempTableName = daoConfig.tablename.concat("_TEMP");
- ArrayList<String> properties =newArrayList();for(int j =0; j < daoConfig.properties.length; j++) {StringcolumnName = daoConfig.properties[j].columnName;if(getColumns(db, tempTableName).contains(columnName)) {
- properties.add(columnName);
- }
- }
- StringBuilder insertTableStringBuilder =newStringBuilder();
- insertTableStringBuilder.append("INSERT INTO ").append(tableName).append(" (");
- insertTableStringBuilder.append(TextUtils.join(",", properties));
- insertTableStringBuilder.append(") SELECT ");
- insertTableStringBuilder.append(TextUtils.join(",", properties));
- insertTableStringBuilder.append(" FROM ").append(tempTableName).append(";");
- StringBuilder dropTableStringBuilder =newStringBuilder();
- dropTableStringBuilder.append("DROP TABLE ").append(tempTableName);
- db.execSQL(insertTableStringBuilder.toString());
- db.execSQL(dropTableStringBuilder.toString());
- }
- }
- privateStringgetTypeByClass(Class type) throws Exception {if(type.equals(String.class)) {return "TEXT";
- }if(type.equals(Long.class) || type.equals(Integer.class) || type.equals(long.class)) {return "INTEGER";
- }if(type.equals(Boolean.class)) {return "BOOLEAN";
- }
- Exception exception =newException(CONVERSION_CLASS_NOT_FOUND_EXCEPTION.concat(" - Class: ").concat(type.toString()));throwexception;
- }
- private static List<String> getColumns(Database db,StringtableName) {
- List<String> columns =newArrayList<>();
- Cursor cursor =null;try{
- cursor = db.rawQuery("SELECT * FROM "+ tableName +" limit 1",null);if(cursor !=null) {
- columns =newArrayList<>(Arrays.asList(cursor.getColumnNames()));
- }
- }catch(Exception e) {
- Log.v(tableName, e.getMessage(), e);
- e.printStackTrace();
- }finally{if(cursor !=null) cursor.close();
- }returncolumns;
- }
- }
这样,只需要在更新字段时,在
类的
- MyOpenHelper
方法中:
- onUpgrade()
- @Override
- publicvoidonUpgrade(Database db, int oldVersion, int newVersion) {
- super.onUpgrade(db, oldVersion, newVersion);//更新表的字段MigrationHelper.getInstance().migrate(db, VideoCacheDao.class);
- }
上面的开源方案,使用起来如此顺手,但不知道细心的你发现没,这个更新辅助类是存在问题的:
- publicvoidmigrate(Database db, Class>... daoClasses) {//1. 备份表generateTempTables(db, daoClasses);//2. 删除所有表DaoMaster.dropAllTables(db,true);//3. 重新创建所有表DaoMaster.createAllTables(db,false);//4. 恢复数据restoreData(db, daoClasses);
- }
发现其每次都是删除所有表、再新建所有表,这意味着:
当我想更新一张表中的某个字段,我却要传入所有的表对应的 XxxDao.class 对象,即使其它表不需要更新,也会经历
、
- 备份
、
- 删除
、
- 新建
的过程,效率低下不说,一不小心还容易出问题。
- 恢复
在上面,我们这么更新表:
- MigrationHelper.getInstance().migrate(db, VideoCacheDao.class);
问题在于,如果你不只是有一张表,在更新某张表的 字段时,如上你只传当前需要更新的表,则其它表的数据都会丢失。明白了吗?没明白的话再好好看看上面的代码。
于是我改造了一下,只需要传入你想更新的表即可:
- publicclassMigrationHelper {
- private static finalStringCONVERSION_CLASS_NOT_FOUND_EXCEPTION ="MIGRATION HELPER - CLASS DOESN'T MATCH WITH THE CURRENT PARAMETERS";
- private static MigrationHelper instance;
- public static MigrationHelper getInstance() {if(instance ==null) {
- instance =newMigrationHelper();
- }returninstance;
- }
- publicvoidmigrate(Database db, Class>... daoClasses) {//1.备份(同上)generateTempTables(db, daoClasses);//2. 只删除需要更新的表(改造)deleteOriginalTables(db, daoClasses);//3. 只创建需要更新的表(改造)
- //DaoMaster.createAllTables(db, false);createOrignalTables(db, daoClasses);//4. 恢复数据restoreData(db, daoClasses);
- }/**
- * 备份要更新的表
- */privatevoidgenerateTempTables(Database db, Class>... daoClasses) {//...}/**
- * 通过反射,删除要更新的表
- */privatevoiddeleteOriginalTables(Database db, Class>... daoClasses){for(Class> daoClass : daoClasses) {try{
- Method method = daoClass.getMethod("dropTable", Database.class, boolean.class);
- method.invoke(null, db,true);
- }catch(IllegalAccessException e) {
- e.printStackTrace();
- }catch(InvocationTargetException e) {
- e.printStackTrace();
- }catch(NoSuchMethodException e) {
- e.printStackTrace();
- }
- }
- }/**
- * 通过反射,重新创建要更新的表
- */privatevoidcreateOrignalTables(Database db, Class>... daoClasses){for(Class> daoClass : daoClasses) {try{
- Method method = daoClass.getMethod("createTable", Database.class, boolean.class);
- method.invoke(null, db,false);
- }catch(IllegalAccessException e) {
- e.printStackTrace();
- }catch(InvocationTargetException e) {
- e.printStackTrace();
- }catch(NoSuchMethodException e) {
- e.printStackTrace();
- }
- }
- }/**
- * 恢复数据
- */privatevoidrestoreData(Database db, Class>... daoClasses) {//...}
- privateStringgetTypeByClass(Class type) throws Exception {//...}
- private static List<String> getColumns(Database db,StringtableName) {//...}
- }
上面,我们通过反射,成功的做到了只
与
- 删除
你传入的表,其它不需要更新的表不需要关心。
- 备份
有关反射的知识,//todo
至此,有关
的更新方案全部介绍完了,最后我们再看看上面遗留的问题:跨版本升级。
- GreenDao
升级数据库时,我们无法保证用户每一版本都会及时更新,可能会跨版本升级,所以一般在
的
- MyOpenHelper
的方法中,我们不能直接忽视数据库版本,像上面那样直接将更新语句怼上去。
- onUpgrade()
这一块就不细说了,下面给出我跨版本升级的方案。假设即将发出去的应用数据库版本为 7,则之前每一版本数据库的变动如下所示。当然,这不是在某一版写的,而是在升级过程中慢慢加上去的:
- @Override
- publicvoidonUpgrade(Database db, int oldVersion, int newVersion) {
- super.onUpgrade(db, oldVersion, newVersion);//判断之前的版本
- switch(oldVersion){case 1:// 无变动
- case 2://新增 VideoCache 表VideoCacheDao.createTable(db,false);case 3:case 4:case 5://新增 User 表UserDao.createTable(db,false);case 6://更新 VideoCache 表字段MigrationHelper.getInstance().migrate(db, VideoCacheDao.class);//更新 User 表字段MigrationHelper.getInstance().migrate(db, UserDao.class);
- }
- }
如果你对跨版本升级还不是很了解,上面的方案理解起来可能会比较困难,建议你多看几遍。
总之,在版本 迭代过程中:
号都要出现在
- version
中;
- case
- break
这样才能确保用户跨版本升级不会出现问题。
以上就是本次分享全部内容,若有任何不当之处,还请指教。
扫描下方二维码,关注我的公众号,及时获取最新文章推送!
来源: http://blog.csdn.net/my_truelove/article/details/70196028