zoukankan      html  css  js  c++  java
  • sqlite升级--浅谈Android数据库版本升级及数据的迁移

    Android开发涉及到的数据库采用的是轻量级的SQLite3,而在实际开发中,在存储一些简单的数据,使用SharedPreferences就足够了,只有在存储数据结构稍微复杂的时候,才会使用数据库来存储。而数据库表的设计往往不是一开始就非常完美,可能在应用版本开发迭代中,表的结构也需要调整,这时候就涉及到数据库升级的问题了。

    数据库升级

    数据库升级,主要有以下这几种情况:

    • 增加表
    • 删除表
    • 修改表 
      • 增加表字段
      • 删除表字段

    增加表和删除表问题不大,因为它们都没有涉及到数据的迁移问题,增加表只是在原来的基础上CRTATE TABLE,而删除表就是对历史数据不需要了,那只要DROP TABLE即可。那么修改表呢?

    其实,很多时候,程序员为了图个方便,最简单最暴力的方法就是,将原来的表删除了然后重新创建新的表,这样就不用考虑其他因素了。但这样对于用户来说,体验是非常不好的,比如:用户当前下载列表正在下载文件,此时进行更新,而新版本有个更新点是升级了下载列表的数据库表,那么用户更新完之后发现下载列表变空了,那么用户看到辛辛苦苦下载的99%文件.avi没来,那不崩溃了,这种体验是非常不好的,分分钟就卸载你的应用。

    那么数据库表升级时,数据迁移就显得非常重要了,那么如何实现呢?

    表升级,数据迁移

    现在开发,为了效率,都会使用第三方,本文数据库方面是基于ORMLite的,所以接下来讨论的都是基于此。

    1 ->2 ->3
    A A+ A
    B B- B
    C C C+

    上表的意思是:版本升级从版本号1升级到2再升级到3,1->2->3,期间表ABC的变化,‘+’表示该表增加了字段,‘-’表示该表删除了字段,例如1升级到2,表A增加了字段,表B删除了字段,表C没有发生变化。

    首先,我们要先理解SQLiteOpenHelper中

    /**
         * Called when the database is created for the first time. This is where the
         * creation of tables and the initial population of the tables should happen.
         *
         * @param db The database.
         */
        public abstract void onCreate(SQLiteDatabase db);

    /**
         * Called when the database needs to be upgraded. The implementation
         * should use this method to drop tables, add tables, or do anything else it
         * needs to upgrade to the new schema version.
         * @param db The database.
         * @param oldVersion The old database version.
         * @param newVersion The new database version.
         */
        public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);

    什么时候调用。文档说得很清楚了,onCreate()是数据库第一次创建的时候调用,而onUpgrade()是当数据库版本升级的时候调用。

    首先,先简单的创建A、B、C三个类,并使用OrmLite注解来创建表

    A.class

    import com.j256.ormlite.field.DatabaseField;
    import com.j256.ormlite.table.DatabaseTable;
    
    @DatabaseTable(tableName = "tb_a")
    public class A {
        @DatabaseField(generatedId = true)
        public int id;
        @DatabaseField
        public String name;
    }

    B.class

    import com.j256.ormlite.field.DatabaseField;
    import com.j256.ormlite.table.DatabaseTable;
    
    @DatabaseTable(tableName = "tb_b")
    public class B {
        @DatabaseField(generatedId = true)
        public int id;
        @DatabaseField
        public String name;
        @DatabaseField
        public String age;
    }

    C.class

    import com.j256.ormlite.field.DatabaseField;
    import com.j256.ormlite.table.DatabaseTable;
    
    @DatabaseTable(tableName = "tb_c")
    public class C {
        @DatabaseField(generatedId = true)
        public int id;
        @DatabaseField
        public String name;
    }
    •  

    创建自己的Helper的MySqliteHelper.class

    import android.content.Context;
    import android.database.sqlite.SQLiteDatabase;
    import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper;
    import com.j256.ormlite.support.ConnectionSource;
    import com.j256.ormlite.table.TableUtils;
    import java.sql.SQLException;
    
    public class MySqliteHelper extends OrmLiteSqliteOpenHelper{
        private final static String DATABASE_NAME="test.db";
        private final static int DATABASE_VERSION = 1;
    
        private static MySqliteHelper mInstance;
    
        public MySqliteHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }
    
        public static MySqliteHelper getInstance(Context context) {
            if (mInstance == null) {
                mInstance= new MySqliteHelper(context);
            }
            return mInstance;
        }
        @Override
        public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
            try {
                TableUtils.createTableIfNotExists(connectionSource,A.class);
                TableUtils.createTableIfNotExists(connectionSource,B.class);
                TableUtils.createTableIfNotExists(connectionSource,C.class);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion, int newVersion) {
    
        }
    }

    创建数据操作的Dao

    import android.content.Context;
    import com.j256.ormlite.dao.Dao;
    import java.sql.SQLException;
    
    public class ADao {
        private Dao<A,Integer> dao;
        public ADao(Context context){
            try {
                dao = MySqliteHelper.getInstance(context).getDao(A.class);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    •  

    BDao、CDao,也类似。 
    运行程序,进行Dao操作,此时就创建数据库test.db,进而执行onCreate()创建表。

    import android.app.Application;
    import android.test.ApplicationTestCase;
    import android.test.suitebuilder.annotation.MediumTest;
    
    import com.helen.andbase.demolist.db.A;
    import com.helen.andbase.demolist.db.ADao;
    
    
    public class ApplicationTest extends ApplicationTestCase<Application> {
        public ApplicationTest() {
            super(Application.class);
        }
    
        @MediumTest
        public void testDao(){
            ADao aDao = new ADao(getContext());
            A a = new A();
            a.name="a";
            aDao.add(a);
    
            BDao bDao = new BDao(getContext());
            B b = new B();
            b.name="a";
            b.age ="18";
            bDao.add(b);
        }
    }
    •  

    这里写图片描述

    将其拷出来,查看数据库。这里使用SQLiteExpertPers进行查看

    这里写图片描述

    这里写图片描述 
    如上图表已创建。接着我们进行数据库升级,将版本号DATABASE_VERSION变为2,表A新增字段age,表B删除字段age,C不变

    @DatabaseTable(tableName = "tb_a")
    public class A {
        @DatabaseField(generatedId = true)
        public int id;
        @DatabaseField
        public String name;
        @DatabaseField
        public String age;
    }
    •  
    @DatabaseTable(tableName = "tb_b")
    public class B {
        @DatabaseField(generatedId = true)
        public int id;
        @DatabaseField
        public String name;
    }
    •  
    @DatabaseTable(tableName = "tb_c")
    public class C {
        @DatabaseField(generatedId = true)
        public int id;
        @DatabaseField
        public String name;
    }
    •  

    简单暴力的解决方法是:

    @Override
        public void onUpgrade(SQLiteDatabase db, ConnectionSource connectionSource, int oldVersion, int newVersion) {
            if(oldVersion < 2){//暂不说明为何要这么判断
                try {
                    TableUtils.dropTable(connectionSource,A.class,true);
                    TableUtils.dropTable(connectionSource,B.class,true);
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            onCreate(db,connectionSource);
        }
    •  

    先将旧的表删除再创建新的表,这是最简单暴力的,但前面提过这不是我们想要的结果。

    将代码改下,

    @Override
        public void onUpgrade(SQLiteDatabase db, ConnectionSource connectionSource, int oldVersion, int newVersion) {
            if(oldVersion < 2){//暂不说明为何要这么判断
                DatabaseUtil.upgradeTable(db,connectionSource,A.class,DatabaseUtil.OPERATION_TYPE.ADD);
            }
            onCreate(db,connectionSource);
        }
    •  

    主要的代码就是封装的DatabaseUtil.class这个类

    import android.database.Cursor;
    import android.database.sqlite.SQLiteDatabase;
    
    import com.j256.ormlite.misc.JavaxPersistence;
    import com.j256.ormlite.support.ConnectionSource;
    import com.j256.ormlite.table.DatabaseTable;
    import com.j256.ormlite.table.TableUtils;
    
    import java.util.Arrays;
    
    public class DatabaseUtil {
        public static final String TAG = "DatabaseUtil.java";
        /**数据库表操作类型*/
        public enum OPERATION_TYPE{
            /**表新增字段*/
            ADD,
            /**表删除字段*/
            DELETE
        }
        /**
         * 升级表,增加字段
         * @param db
         * @param clazz
         */
        public static <T> void upgradeTable(SQLiteDatabase db,ConnectionSource cs,Class<T> clazz,OPERATION_TYPE type){
            String tableName = extractTableName(clazz);
    
            db.beginTransaction();
            try {
    
                //Rename table
                String tempTableName = tableName + "_temp";
                String sql = "ALTER TABLE "+tableName+" RENAME TO "+tempTableName;
                db.execSQL(sql);
    
                //Create table
                try {
                    sql = TableUtils.getCreateTableStatements(cs, clazz).get(0);
                    db.execSQL(sql);
                } catch (Exception e) {
                    e.printStackTrace();
                    TableUtils.createTable(cs, clazz);
                }
    
                //Load data
                String columns;
                if(type == OPERATION_TYPE.ADD){
                    columns = Arrays.toString(getColumnNames(db,tempTableName)).replace("[","").replace("]","");
                }else if(type == OPERATION_TYPE.DELETE){
                    columns = Arrays.toString(getColumnNames(db,tableName)).replace("[","").replace("]", "");
                }else {
                    throw new IllegalArgumentException("OPERATION_TYPE error");
                }
                sql = "INSERT INTO "+tableName +
                        " ("+ columns+") "+
                        " SELECT "+ columns+" FROM "+tempTableName;
                db.execSQL(sql);
    
                //Drop temp table
                sql = "DROP TABLE IF EXISTS "+tempTableName;
                db.execSQL(sql);
    
                db.setTransactionSuccessful();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                db.endTransaction();
            }
        }
    
    
        /**
         * 获取表名(ormlite DatabaseTableConfig.java)
         * @param clazz
         * @param <T>
         * @return
         */
        private static <T> String extractTableName(Class<T> clazz) {
            DatabaseTable databaseTable = clazz.getAnnotation(DatabaseTable.class);
            String name ;
            if (databaseTable != null && databaseTable.tableName() != null && databaseTable.tableName().length() > 0) {
                name = databaseTable.tableName();
            } else {
                /*
                 * NOTE: to remove javax.persistence usage, comment the following line out
                 */
                name = JavaxPersistence.getEntityName(clazz);
                if (name == null) {
                    // if the name isn't specified, it is the class name lowercased
                    name = clazz.getSimpleName().toLowerCase();
                }
            }
            return name;
        }
    
        /**
         * 获取表的列名
         * @param db
         * @param tableName
         * @return
         */
        private static String[] getColumnNames(SQLiteDatabase db,String tableName){
            String[] columnNames = null;
            Cursor cursor = null;
            try {
                cursor = db.rawQuery("PRAGMA table_info("+tableName+")",null);
                if(cursor != null){
                    int columnIndex = cursor.getColumnIndex("name");
                    if(columnIndex == -1){
                        return null;
                    }
    
                    int index = 0;
                    columnNames = new String[cursor.getCount()];
                    for(cursor.moveToFirst();!cursor.isAfterLast();cursor.moveToNext()){
                        columnNames[index] = cursor.getString(columnIndex);
                        index++;
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                if(cursor != null) {
                    cursor.close();
                }
            }
            return columnNames;
        }
    
    }
    •  
    •  

    upgradeTable方法里采用的是数据库事务,利用事务的原子特性,保证所有的SQL能全部执行完成。主要思路是:首先将原来的表进行改名称rename table(临时表),接着创建新的表create table,再者将旧表内的数据迁移到新表内,最后drop table删除临时表。

    表的增加字段和删除字段,在迁移数据的时候,主要区别在于字段的来源不同。比如:A表新增了age字段,这时候columns变量的获取是根据旧表来的,这是构造的sql语句是

    sql = "INSERT INTO tb_a (id,name) SELECT id,name FROM tb_a_temp";
    • 1
    • 1

    而B表是删除age字段的,columns变量的获取是根据新表来的,其构造的sql语句是

    sql = "INSERT INTO tb_b (id) SELECT id FROM tb_b_temp";
    • 1
    • 1

    再次执行ApplicationTest->testDao

    @MediumTest
        public void testDao(){
            ADao aDao = new ADao(getContext());
            A a = new A();
            a.name="a";
            a.age = "20";
            aDao.add(a);
    
            BDao bDao = new BDao(getContext());
            B b = new B();
            b.name="b";
            bDao.add(b);
        }
    •  

    再查看下数据

    这里写图片描述

    这里写图片描述

    可以看到表A、表B的历史数据还是存在的。

    然后我们再将数据库升级到版本号为3,这时候要考虑到用户的多种情况,从1->3,从2->3这两种情况,但不是每次升级都重复之前的操作的,比如用户1之前已经从1升级到2了,这次是要从2升级到3,而用户2,一直用的是老版本1,他觉得,嗯,这次这个版本升级的内容不错,决定升级了,那么他是从1直接升级到3的,所以他们两者经历的版本不一样,数据库升级的策略也会有所不用,那就要区分开来考虑了。

    这次的升级是将表C添加了sex字段

    import com.j256.ormlite.field.DatabaseField;
    import com.j256.ormlite.table.DatabaseTable;
    
    @DatabaseTable(tableName = "tb_c")
    public class C {
        @DatabaseField(generatedId = true)
        public int id;
        @DatabaseField
        public String name;
        @DatabaseField
        public String sex;
    }

    然后在onUpgrade进行逻辑判断

        @Override
        public void onUpgrade(SQLiteDatabase db, ConnectionSource connectionSource, int oldVersion, int newVersion) {
            if(oldVersion < 2){
                DatabaseUtil.upgradeTable(db,connectionSource,A.class,DatabaseUtil.OPERATION_TYPE.ADD);
                DatabaseUtil.upgradeTable(db,connectionSource,B.class,DatabaseUtil.OPERATION_TYPE.DELETE);
            }
            if(oldVersion < 3){
                DatabaseUtil.upgradeTable(db,connectionSource,C.class,DatabaseUtil.OPERATION_TYPE.ADD);
            }
            onCreate(db,connectionSource);
        }
    •  

    这样,如果你是从1升级到3,那么两个if语句都会执行,而如果是从2升级到3,那么只有if(oldVersion < 3)这个分支会执行。最后,如果只是新增全新的表D,那么只要在onCreate内多写句TableUtils.createTableIfNotExists(connectionSource, D.class);就可以啦,不要忘记版本号要+1~

    总结

    本文讨论的数据迁移,是基于新旧两个表之间逻辑性不强,不牵涉到业务情景的情况下。比如,表A新增的字段user_id为用户id,这个字段是用来标记数据来源于哪个用户的,检索的时候,user_id是用于检索条件的,那么由于旧数据转移到新表中user_id默认是空的,这时候旧数据可能相当于不起作用了,虽然可以通过设置默认值,但其需要根据具体业务场景进行设置,因此就失去其灵活性了。

  • 相关阅读:
    通过PROFINET网络实现SINAMICS 120的PN IO OPC通讯,起动及调速控制
    Python datetime获取当前年月日时分秒
    计算机网络:套接字(Socket)| Python socket实现服务器端与客户端通信,使用TCP socket阿里云ECS服务器与本机通信
    Ubuntu16.04安装、卸载宝塔软件
    Ubuntu一键安装LAMP,LNMP
    STM32使用K型热电偶测温:运算放大器+内置ADC+K型热电偶分度表+中间温度定律 | K型热电偶的温度-热电势曲线
    盘点几种DIY加密狗的制作方法,适用于穿越机模拟器
    变频器通讯参数PKW和PZD的含义
    穿越机从0到起飞:选件
    西门子S7-1200PLC不让下载一直报“模块具有激活的测试和调试功能,防止下载到设备”解决方法
  • 原文地址:https://www.cnblogs.com/awkflf11/p/6033074.html
Copyright © 2011-2022 走看看