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

sqlite升级--浅谈Android数据库版本升级及数据的迁移的更多相关文章

  1. 浅谈Android数据库DBFlow

    一.项目配置 1.根目录下的build.gradle 中添加 maven { url "https://www.jitpack.io" } 如下: classpath 'com.n ...

  2. [Android 泥水匠] Android基础 之一:浅谈Android架构到HelloWorld案例的剖析

    作者:泥沙砖瓦浆木匠网站:http://blog.csdn.net/jeffli1993个人签名:打算起手不凡写出鸿篇巨作的人,往往坚持不了完成第一章节. 交流QQ群:[编程之美 365234583] ...

  3. 浅谈Android保护技术__代码混淆

    浅谈Android保护技术__代码混淆   代码混淆 代码混淆(Obfuscated code)亦称花指令,是将计算机程序的代码,转换成一种功能上等价,但是难于阅读和理解的形式的行为.将代码中的各种元 ...

  4. 浅谈Android应用保护(一):Android应用逆向的基本方法

    对于未进行保护的Android应用,有很多方法和思路对其进行逆向分析和攻击.使用一些基本的方法,就可以打破对应用安全非常重要的机密性和完整性,实现获取其内部代码.数据,修改其代码逻辑和机制等操作.这篇 ...

  5. 安卓开发_浅谈Android动画(四)

    Property动画 概念:属性动画,即通过改变对象属性的动画. 特点:属性动画真正改变了一个UI控件,包括其事件触发焦点的位置 一.重要的动画类及属性值: 1.  ValueAnimator 基本属 ...

  6. 浅谈Android应用性能之内存

    本文来自http://blog.csdn.net/liuxian13183/ ,引用必须注明出处! 文/ jaunty [博主导读]在Android开发中,不免会遇到许多OOM现象,一方面可能是由于开 ...

  7. 浅谈Android五大布局

    Android的界面是有布局和组件协同完成的,布局好比是建筑里的框架,而组件则相当于建筑里的砖瓦.组件按照布局的要求依次排列,就组成了用户所看见的界面.Android的五大布局分别是LinearLay ...

  8. [转]浅谈Android五大布局(二)——RelativeLayout和TableLayout

    在浅谈Android五大布局(一)中已经描述了LinearLayout(线性布局).FrameLayout(单帧布局)和AbsoulteLayout(绝对布局)三种布局结构,剩下的两种布局Relati ...

  9. [转]浅谈Android五大布局(一)——LinearLayout、FrameLayout和AbsoulteLayout

    Android的界面是有布局和组件协同完成的,布局好比是建筑里的框架,而组件则相当于建筑里的砖瓦.组件按照布局的要求依次排列,就组成了用户所看见的界面.Android的五大布局分别是LinearLay ...

随机推荐

  1. 函数中的static静态变量

    静态变量仅在局部函数域中存在且只被初始化一次,当程序执行离开此作用域时,其值不会消失,会使用上次执行的结果. <?php function testStatic($start,$end){ st ...

  2. 去掉tableView的header view的粘黏性

    有的项目中,需要使用tableview的headerview,但是想让headerview粘在最顶不,希望和cell一起滚动,可以试试下面的代码来帮你实现这个需求: - (void)scrollVie ...

  3. 【JS】数字转大写中文

    原文参考 逛到一道面试题,数字转大写中文的,搜索学习并记录于此. //自动转换数字金额为大小写中文字符,返回大小写中文字符串,最大处理到999兆 function changeMoneyToChine ...

  4. 15 cvpr An Improved Deep Learning Architecture for Person Re-Identification

    http://www.umiacs.umd.edu/~ejaz/ * 也是同时学习feature和metric * 输入一对图片,输出是否是同一个人 * 包含了一个新的层: include a lay ...

  5. PHP--------TP中的ajax请求

    PHP--------TP中的ajax请求 以jQuery中的ajax为例: (1)引入jQuery 通过模板替换表示路径,我们可以自定义模板替换变量(在config中定义) /*自定义模板替换标签* ...

  6. 初识Linux—1

    1,Ctrl+C作用是终止当前的命令 2,ps显示目前正在执行的程序(命令)(process status) 3,退出是exit,连续按exit,最终会关闭终端 4,Root是管理员,其他的用户都是由 ...

  7. CentOS6.5上Oracle11gR2静默安装

    一.环境准备环境 操作系统:CentOS release 6.5 (Final) 内核版本:2.6.32-431.el6.x86_64 物理内存:2G(必须大于1G) swap分区:3G(必须大于3G ...

  8. 各种模板(part 2)

    堆: using namespace dui //堆 { #include<queue> //需要的库 priority_queue < int > Q; //定义一个Q的大根 ...

  9. zip伪加密文件分析(进阶版)

    作者近日偶然获得一misc题,本来以为手到擒来,毕竟这是个大家都讨论烂了的题,详情访问链接http://blog.csdn.net/ETF6996/article/details/51946250.既 ...

  10. C#中String转int问题

    String转int主要有四种方法 1. int.Parse()是一种类容转换:表示将数字内容的字符串转为int类型. 如果字符串为空,则抛出ArgumentNullException异常: 如果字符 ...