浅谈Android数据库版本升级及数据的迁移

标签: android 数据库 版本 | 发表时间:2015-07-29 16:21 | 作者:a631855639
出处:http://blog.csdn.net

概述

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";

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

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

再次执行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默认是空的,这时候旧数据可能相当于不起作用了,虽然可以通过设置默认值,但其需要根据具体业务场景进行设置,因此就失去其灵活性了。

作者:a631855639 发表于2015/7/29 16:21:06 原文链接
阅读:56 评论:0 查看评论

相关 [android 数据库 版本] 推荐:

浅谈Android数据库版本升级及数据的迁移

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

Android SQLite数据库版本升级原理解析

- - CSDN博客推荐文章
Android使用SQLite数据库保存数据,那数据库版本升级是怎么回事呢,这里说一下. 安装v1.0,假设v1.0版本只有一个account表,这时走继承SQLiteOpenHelper的onCreate,不走onUpgrade. 1、v1.0(直接安装v1.0). 1、v1.0   -->  v2.0              不走onCreate,走onUpgrade.

Android数据库导出工具—ADEL

- - FreebuF.COM
ADEL(Android Data Extractor Lite)这款工具可自动导出Android 2.x移动设备的SQLite数据库,并且释放下载文件中的存储内容. 以下数据库会被ADEL导出 telephone and SIM-card information (e. IMSI and serial number) telephone book and call lists, calendar entries, SMS messages, GPS locations from different sources on the smartphone.

Android数据库升级实例

- - BlogJava-qileilove
  Andoird的SQLiteOpenHelper类中有一个onUpgrade方法. 经过实践,解决了我一连串的疑问:. 帮助文档里说的“数据库升级”是指什么.   你开发了一个程序,当前是1.0版本. 到1.1版本时,你在数据库的某个表中增加了一个字段. 那么软件1.0版本用的数据库在软件1.1版本就要被升级了.

Android中数据库升级onUpgrade方法说明

- - BlogJava-qileilove
Andoird的SQLiteOpenHelper类中有一个onUpgrade方法. 经过实践,解决了我一连串的疑问:. 帮助文档里说的“数据库升级”是指什么.   你开发了一个程序,当前是1.0版本. 到1.1版本时,你在数据库的某个表中增加了一个字段. 那么软件1.0版本用的数据库在软件1.1版本就要被升级了.

将android程序中的数据库导出到SD卡

- - CSDN博客移动开发推荐文章
String DATABASE_NAME = "数据库文件名称";. InputStream inStream = new FileInputStream(oldPath); // 读入原文件. bytesum += byteread; // 字节数 文件大小. System.out.println("复制单个文件操作出错");.

使用flyway对应用的数据库版本控制

- - Java - 编程语言 - ITeye博客
转载自:http://kkuang.net/why-db-migration-tools-flyway/  . 为什么要使用DB migration tools. 我们可以想象,多位开发人员,多个数据库环境. 先前有用过mybatis中的migration,但经常出现莫名其名的异常,并且也不支持多条SQL在1个文件中.

数据库版本控制完全指南

- - 极客521 | 极客521
在这个充斥着大数据与商业智能的新代时,唯一不变的技术就是变化,尤其是在数据库方面. 出于数据统计、继续增加的对服务的需求,以及规定制度等方面的原因,几乎每天都有业务方面的变更需求,这些都会对数据库产生变更需求. 当数据库变更发生时,能否从自动化中获得更大的敏捷性,以较少的资源实现较多的功能,正是那些具有高度竞争力的世界级企业在芸芸众生中脱颖而出的关键因素.

MySQL数据库开源软件版本 生产环境GA版本如何选择

- - MySQLOPS 数据库与运维自动化技术分享
很多技术朋友向我咨询过关于选择MySQL数据库软件产品的版本事宜,他们对于开源软件产品的版本选择没有头绪,不知道从何下手. 不过,我们本文主要是介绍开源数据库产品的版本如何选择,如何选择生产环境的MySQL数据库版本. MySQL数据库版本的选择需要经过那些工序. 顺道分享下我们DBA团队是如何为生产环境,选择合适的MySQL数据库版本.

LibreOffice将发展Web、iOS和Android版本

- Heng Yang - cnBeta.COM
OpenOffice.org的分支项目LibreOffice的开发方The Document Foundation今天宣布将尝试在Web浏览器和iOS、Android移动设备上运行这一办公软件,预计这些产品将在2012年年底到2013年初正式发布.