绝对值得一看的 Android 数据库升级攻略

阅读 4810
收藏 137
2017-01-03
原文链接:blog.csdn.net

Android数据存储方式

 Android提供了五种方式来让用户保存持久化应用程序数据。根据自己的需求来做选择,比如数据是否是应用程序私有的,是否能被其他程序访问,需要多少数据存储空间等,分别是:   

  • SharedPreferences存储
  • 文件存储
  • SQLite数据库存储
  • ContentProvider存储
  • 网络存储

当存取数据比较复杂时,我们会选择SQLite数据库进行存储,下面我们会介绍一下在使用SQLite时遇到的问题及相应的解决方案

Android数据库升级完整解决方案

数据库升级的意义

  • 数据库表的设计往往不是一开始就非常完美,可能在应用版本开发迭代中,如新功能发布,业务逻辑变更,早期的数据库结构无法满足新版本的需求

方法

  • 让用户卸载老版本再安装新的程序; 缺点:可操作性低,软件卸载会造成老数据的丢失
  • 软件自行更新数据库结构。 可取:作为开发者必须妥善处理数据库的升级问题

解决方案

数据库的升级,无外乎以下几种情况:

  • 增加表
  • 删除表

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

  • 修改表(修改表字段,删除表字段,增加表字段)

①可能一个比较暴力的做法:就是将原来的表删除了然后重新创建新的表,这样就不用考虑其他因素了。这种方法不可取,会造成数据丢失。 ②明智的做法: 表升级,数据也要迁移

首先需要理解SQLiteOpenHelper中的两个重要函数:

public abstract class SQLiteOpenHelper {
   //数据库第一次创建的时候调用
   public abstract void onCreate(SQLiteDatabase db);
   //当数据库版本升级的时候调用
   public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
}

然后来看看具体如何来 修改表定义

SQLite数库对ALTER TABLE命令支持非常有限,只能在表末尾添加列,不能修改列定义,不能删除已有的列。那么如果要修改表呢?我们可以采用临时表的办法。具体来说有四步:

  • 将现有表重命名为临时表;
  • 创建新表;
  • 将临时表的数据导入新表(注意处理修改的列);
  • 删除临时表。

代码案例如下:

    @Override
    public void onCreate(SQLiteDatabase db) {
        createAllTables(db);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        switch (oldVersion) {
            case 1:
            //如果老版本是1,则创建X表
            createTableX(db);
            case 2:
            //如果老版本是2,则修改表X的结构(例如添加两个字段)
            upgradeTables(db,"X","ColA, ColB, ColC, ... ColN");
                break;
            default:
                dropAllTables(db);
                createAllTables(db);
                break;
        }
    }

有个关于小程序之switch…case…break的小片段需要注意一下,case 1之后是没有break的

upgradeTables方法里采用的是数据库事务,利用事务的原子特性(确保工作单位内的所有操作都成功完成,否则,事务会在出现故障时终止,之前的操作也会回滚到以前的状态)

/** 
 * Upgrade tables. In this method, the sequence is: 
 * [1] Rename the specified table as a temporary table. 
 * [2] Create a new table which name is the specified name. 
 * [3] Insert data into the new created table, data from the temporary table. 
 * [4] Drop the temporary table. 
 * 
 * @param db The database. 
 * @param tableName The table name. 
 * @param columns The columns range, format is "ColA, ColB, ColC, ... ColN"; 
 */  
protected void upgradeTables(SQLiteDatabase db, String tableName, String columns)  
{  
    try  
    {  
        db.beginTransaction();  

        // 1, Rename table.  
        String tempTableName = tableName + "_temp";  
        String sql = "ALTER TABLE " + tableName +" RENAME TO " + tempTableName;  
        execSQL(db, sql, null);  

        // 2, Create table.  
        //onCreateTable(db); 
        createNewTableX(db);

        // 3, Load data  
        sql =   "INSERT INTO " + tableName +  
                " (" + columns + ") " +  
                " SELECT " + columns + " FROM " + tempTableName;  

        execSQL(db, sql, null);  

        // 4, Drop the temporary table.  
        execSQL(db, "DROP TABLE IF EXISTS " + tempTableName, null);  

        db.setTransactionSuccessful();  
    }  
    catch (SQLException e)  
    {  
        e.printStackTrace();  
    }  
    catch (Exception e)  
    {  
        e.printStackTrace();  
    }  
    finally  
    {  
        db.endTransaction();  
    }  
}  

跨越版本的升级

处理好了单个版本的升级,还有一个更加棘手的问题:如果应用程序发布了多个版本,以致出现了三个以上数据库版本, 如何确保所有的用户升级应用后数据库都能用呢?有两种方式:

方式一:确定 相邻版本 的差别,从版本1开始依次迭代更新,先执行v1到v2,再v2到v3……

方式二:为 每个版本 确定与现在数据库的差别,为每个case撰写专门的升级代码。

方式一的优点是每次更新数据库的时候只需要在onUpgrade方法的末尾加一段从上个版本升级到新版本的代码,易于理解和维护,缺点是当版本变多之后,多次迭代升级可能需要花费不少时间,增加用户等待;

方式二的优点则是可以保证每个版本的用户都可以在消耗最少的时间升级到最新的数据库而无需做无用的数据多次转存,缺点是强迫开发者记忆所有版本数据库的完整结构,且每次升级时onUpgrade方法都必须全部重写。 以上简单分析了两种方案的优缺点,它们可以说在花费时间上是刚好相反的,至于如何取舍,可能还需要结合具体情况分析。

上述升级案例中使用的是 方式一

小插曲

Failed to open database

  • 某些情况下可以通过重启手机解决,初步分析原因可能是【sdcard状态异常】、【数据库db文件被其它应用占用了,如被手机上的db查看器占用了】

使用SQLiteOpenHelper的getReadableDatabase()获得的数据库能不能做写的操作

答案:

  • 可以! 不要被Readable的意思误导啦,readable是可读的意思,但不代表不能写哦。
  • 其实就是考察getReadableDatabase()和getWriteableDatabase()的区别

源码解读区别:

  • 查看源码,发现getReadableDatabase()和getWriteableDatabase()都是调用getDatabaseLocked(boolean writeable) 方法,传不同的参数
  • getReadableDatabase() 会获取用于操作SQLiteDatabase的实例。 getReadableDatabase()会先以读写方式打开数据库,若数据库磁盘空间满了,打开失败,会继续尝试以只读方式打开。若磁盘空间有了,会关闭只读数据库对象,返回可读写数据库对象。
  • getWriteableDatabase()也是会以读写方式打开数据库,如果磁盘满了,会抛异常,不会返回数据库对象。
  • 其实就是getReadableDatabase()会在抛异常的时候以只读模式打开数据库。而getWritableDatabase()不会
public SQLiteDatabase getWritableDatabase() {
        synchronized (this) {
            return getDatabaseLocked(true);
        }
    }
 public SQLiteDatabase getReadableDatabase() {
        synchronized (this) {
            return getDatabaseLocked(false);
        }
    }

    private SQLiteDatabase getDatabaseLocked(boolean writable) {
        if (mDatabase != null) {
            if (!mDatabase.isOpen()) {
                // Darn!  The user closed the database by calling mDatabase.close().
                mDatabase = null;
            } else if (!writable || !mDatabase.isReadOnly()) {
                // The database is already open for business.
                return mDatabase;
            }
        }

        if (mIsInitializing) {
            throw new IllegalStateException("getDatabase called recursively");
        }

        SQLiteDatabase db = mDatabase;
        try {
            mIsInitializing = true;

            if (db != null) {
                if (writable && db.isReadOnly()) {
                    db.reopenReadWrite();
                }
            } else if (mName == null) {
                db = SQLiteDatabase.create(null);
            } else {
                try {
                    if (DEBUG_STRICT_READONLY && !writable) {
                    //如果以严格的只读方式打开
                    //获取db文件的路径
                        final String path = mContext.getDatabasePath(mName).getPath();
                        //直接打开数据库
                        db = SQLiteDatabase.openDatabase(path, mFactory,
                                SQLiteDatabase.OPEN_READONLY, mErrorHandler);
                    } else {
                    //如果以读写方式打开
                    //打开或创建数据库
                        db = mContext.openOrCreateDatabase(mName, mEnableWriteAheadLogging ?
                                Context.MODE_ENABLE_WRITE_AHEAD_LOGGING : 0,
                                mFactory, mErrorHandler);
                    }
                } catch (SQLiteException ex) {
                //getWritableDatabase()会在抛异常的时候抛出异常
                    if (writable) {
                        throw ex;
                    }
                    Log.e(TAG, "Couldn't open " + mName
                            + " for writing (will try read-only):", ex);
                    final String path = mContext.getDatabasePath(mName).getPath();
                    //getReadableDatabase()会在抛异常的时候以只读模式打开数据库
                    db = SQLiteDatabase.openDatabase(path, mFactory,
                            SQLiteDatabase.OPEN_READONLY, mErrorHandler);
                }
            }

          ...
    }

参考链接

flyingcat2013.blog.51cto.com/7061638/153… www.2cto.com/kf/201507/4…

评论