SQLiteOpenHelper版本升级

SQLiteOpenHelper 基本用法
java

咱们在编写数据库应用软件时,须要考虑这样的问题:由于咱们开发的软件可能会安装在不少用户的手机上,若是应用使用到了SQLite数据库,咱们必须在用户初次使用软件时建立出应用使用到的数据库表结构及添加一些初始化记录,另外在软件升级的时候,也须要对数据表结构进行更新。那么,咱们如何才能实如今用户初次使用或升级软件时自动在用户的手机上建立出应用须要的数据库表呢?总不能让咱们在每一个须要安装此软件的手机上经过手工方式建立数据库表吧?由于这种需求是每一个数据库应用都要面临的,因此在Android系统,为咱们提供了一个名为SQLiteOpenHelper的抽象类,必须继承它才能使用,它是经过对数据库版本进行管理来实现前面提出的需求。android


下面咱们来看一下Android SQLite 最经常使用的几个类和那些方法:sql


1、SQLiteOpenHelper :数据库


onCreate(SQLiteDatabase db);数组

     用于初次使用软件时生成数据库表。ide

当调用SQLiteOpenHelper的getWritableDatabase()或者getReadableDatabase()方法获取用于操做数据库的SQLiteDatabase实例的时候,函数

若是数据库不存在,Android系统会自动生成一个数据库,接着调用onCreate()方法,onCreate()方法在初次生成数据库时才会被调用,this

在onCreate()方法里能够生成数据库表结构及添加一些应用使用到的初始化数据spa


onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);指针

在数据库的版本发生变化时会被调用,通常在软件升级时才需改变版本号


getWritableDatabase();

getReadableDatabase();

两方法均可以获取一个用于操做数据库的SQLiteDatabase实例。但getWritableDatabase() 方法以读写方式打开数据库,一旦数据库的磁盘空间满了,数据库就只能读而不能写,假若使用getWritableDatabase()打开数据库就会出错。getReadableDatabase()方法先以读写方式打开数据库,若是数据库的磁盘空间满了,就会打开失败,当打开失败后会继续尝试以只读方式打开数据库。




2、SQLiteDatabase:


一、db.insert(table, nullColumnHack, values)

第一个参数是表名称,第二个参数是空列的默认值,第三个参数是ContentValues类型的一个封装了列名称和列值的Map;


二、db.delete(table, whereClause, whereArgs)

第一个参数是表名称,第二个参数是删除条件,第三个参数是删除条件值数组


三、db.update(table, values, whereClause, whereArgs)

第一个参数是表名称,第二个参数是更行列ContentValues类型的键值对(Map),第三个参数是更新条件(where字句),第四个参数是更新条件数组


四、db.query(table, columns, selection, selectionArgs, groupBy, having, orderBy)(下面有对该方法详细讲解)


五、db.execSQL(sql) // 执行任何SQL语句


六、db.rawQuery(sql, selectionArgs)


对第四个方法详细讲解:


Cursor  query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit)

各个参数的意义说明:

①table:表名称

②columns:列名称数组

③selection:条件字句,至关于where

④selectionArgs:条件字句,参数数组

⑤groupBy:分组列

⑥having:分组条件

⑦orderBy:排序列

⑧limit:分页查询限制



3、Cursor:

Cursor是一个游标接口,提供了遍历查询结果的方法,如移动指针方法move(),得到列值方法getString()等.


Cursor游标经常使用方法:


getCount()   总记录条数

isFirst()     判断是否第一条记录

isLast()      判断是否最后一条记录

moveToFirst()    移动到第一条记录

moveToLast()    移动到最后一条记录

move(int offset)   移动到指定记录

moveToNext()    移动到下一条记录

moveToPrevious()    移动到上一条记录

getColumnIndexOrThrow(String columnName)  根据列名称得到列索引

getInt(int columnIndex)   得到指定列索引的int类型值

getString(int columnIndex)   得到指定列缩影的String类型值


利用onUpgrade 升级数据库版本

1.

代码片断,双击复制
01
private static final int VERSION = 2 ;

      
?
代码片断,双击复制
01
02
03
04
05
06
07
08
09
10
11
12
13
14
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    db.execSQL( "drop table if exists " + Tables.TERM);
    db.execSQL( "drop table if exists " + Tables.PAPER);
    db.execSQL( "drop table if exists " + Tables.CHECKPOINT);
    db.execSQL( "drop table if exists " + Tables.QUESTION);
    db.execSQL( "drop table if exists " + Tables.QUESTION_ATTRIBUTE);
    db.execSQL( "drop table if exists " + Tables.ALTERNATIVE);
    db.execSQL( "drop table if exists " + Tables.ALTERNATIVE_ATTRIBUTE);
    db.execSQL( "drop table if exists " + Tables.MATRIXROW);
    db.execSQL( "drop table if exists " + Tables.MATRIXROW_ATTRIBUTE);
    db.execSQL( "drop table if exists " + Tables.QUESTIONNAIRE);
    onCreate(db);
}
?
代码片断,双击复制
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//定义升级函数
private void upgradeDatabaseToVersion1(SQLiteDatabase db) {
         // Add 'new' column to mytable table.
         db.execSQL( "ALTER TABLE mytable ADD COLUMN new TEXT" );
    }
 
//重写onUpgrade
public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
         Log.w(TAG, "Upgrading database from version " + oldVersion
                + " to " + currentVersion + "." );
 
         switch (oldVersion) {
         case 0 :
             if (currentVersion <= 1 ) {
                return ;
             }
 
             db.beginTransaction();
             try {
                upgradeDatabaseToVersion1(db);
                db.setTransactionSuccessful();
             } catch (Throwable ex) {
                Log.e(TAG, ex.getMessage(), ex);
                break ;
             } finally {
                db.endTransaction();
             }
 
            return ;
    }
 
    Log.e(TAG, "Destroying all old data." );
    dropAll(db);
    onCreate(db);
}
2.

在android应用程序须要升级时,若是以前的数据库表结构发生了变化或者新添加了表,就须要对数据库进行升级,并保留原来的数据库数据。

 程序如何知道数据库须要升级?

SQLiteOpenHelper类的构造函数有一个参数是int version,它的意思就是指数据库版本号。好比在软件1.0版本中,咱们使用SQLiteOpenHelper访问数据库时,该参数为1,那么数据库版本号1就会写在咱们的数据库中。

到了1.1版本,咱们的数据库须要发生变化,那么咱们1.1版本的程序中就要使用一个大于1的整数来构造SQLiteOpenHelper类,用于访问新的数据库,好比2。

当咱们的1.1新程序读取1.0版本的老数据库时,就发现老数据库里存储的数据库版本是1,而咱们新程序访问它时填的版本号为2,系统就知道数据库须要升级。

什么时候触发数据库升级?如何升级?

当系统在构造SQLiteOpenHelper类的对象时,若是发现版本号不同,就会自动调用onUpgrade函数,让你在这里对数据库进行升级。根据上述场景,在这个函数中把老版本数据库的相应表中增长字段,并给每条记录增长默认值便可。

新版本号和老版本号都会做为onUpgrade函数的参数传进来,便于开发者知道数据库应该从哪一个版本升级到哪一个版本。

升级完成后,数据库会自动存储最新的版本号为当前数据库版本号。

下面举例写出具体过程:

若是我上一个版本的数据库表结构没发生变化,可是新增了两张表,并且以前有一张表中默认有4条数据,如今新版本默认有11条数据,那么该怎么操做呢,假设表A发生了变化,而且新建了表B、C

1.首先咱们须要把原来的数据库表重命名一下

public static final String TEMP_SQL_CREATE_TABLE_SUBSCRIBE = "alter table "
            + A + " rename to temp_A";

原来的表结构是:

private static final String SQL_CREATE_TABLE_SUBSCRIBE = "create table  if not exists "
            + A + "(id integer primary key autoincrement,code text not null,name text,username text)";

2.而后把备份表temp_A中的数据copy到新建立的数据库表A中,这个表A没发生结构上的变化

public static final String INSERT_SUBSCRIBE = "select 'insert into A (code,name,username,tablename) 
values ('''||code||''','''||name||''',''cnki'','''||tablename||'''')' as insertSQL from temp_A";

 

3.此时临时表中的数据已经所有复制到了表A中,可是我以前的表A中有四条默认的数据,用户可能删了,可能又增长了新的数据,那我无论用户什么操做,我都把这4条删除掉,而后从新添加一次,这样就保证了以前的四条数据还在新的数据表中。

这是我以前的四条数据,我先找出来:

public static final String[] arrWhereAct = {
            "where code ='K174' and tablename = 'phonepaper'",
            "where code ='GMRB' and tablename = 'newspaper'",
            "where code ='XJSJ' and tablename = 'magazine'",
            "where code ='JTKL' and tablename = 'magazine'" };

 

4.删除备份表

public static final String DELETE_TEMP_SUBSCRIBE = "delete from temp_A ";
    public static final String DROP_TEMP_SUBSCRIBE = "drop table if exists temp_A";

5.而后把数据库版本号改成比以前高的版本号,在OnUpgrade方法中执行上述语句就行,具体以下:

复制代码
@Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        
        for (int j = oldVersion; j <= newVersion; j++) {
            switch (j) {
            case 2:
          //建立临时表
db.execSQL(TEMP_SQL_CREATE_TABLE_SUBSCRIBE);           //执行OnCreate方法,这个方法中放的是表的初始化操做工做,好比建立新表之类的
onCreate(db);           //删除以前的表里面的那4条默认的数据
for (int i = 0; i < arrWhereAct.length; i++) { db.execSQL(DELETE_TEMP_SUBSCRIBE + arrWhereAct[i]); } //将临时表中的数据放入表A
         Cursor cursor
= db.rawQuery(INSERT_SUBSCRIBE, null); if (cursor.moveToFirst()) { do { db.execSQL(cursor.getString(cursor .getColumnIndex("insertSQL"))); } while (cursor.moveToNext()); } cursor.close();           //将临时表删除掉
db.execSQL(DROP_TEMP_SUBSCRIBE);
break; default: break; } } }
复制代码

为何要在方法里写for循环,主要是考虑到夸版本升级,好比有的用户一直不升级版本,数据库版本号一直是1,而客户端最新版本其实对应的数据库版本已是4了,那么我中途可能对数据库作了不少修改,经过这个for循环,能够迭代升级,不会发生错误。

具体实现就是这样,望高手吐槽!


全面控制数据库版本升级


一,前言

  没有采用Android自身提供的那一套数据库操做方式。而是想对SQLite数据库文件有更全面的控制,包括随时导出数据库文件修改表结构,增删数据等等。这样一来虽然在开放中获得很多便利,可是也带来了数据库升级的一些问题。

  后来不得已采用了一种方案,能够解决问题,现将方案的所有实现细节记录下来。最后也会提出一些我认为有问题的地方。

  二,数据库文件拷贝

  程序不负责数据库的建立,SQLite数据库文件是在外部建立好的。程序启动阶段拷贝进SD卡。以达到对数据库结构的全面控制。
  数据库文件存放位置见附件图片。

  Java代码
  

  1. public void copyDBFile() {
  2.   // 数据库路径
  3.   if (!FileOperator.checkFile(SysConst.DB_PATH)) {
  4.   boolean result = FileOperator.write2Sdcard(activity,
  5.   R.raw.scpip_collection, SysConst.DB_PATH);
  6.   Debug.log("无数据库文件,首次拷贝" + result);
  7.   if (!result) {
  8.   throw new IllegalAccessError(activity
  9.   .getString(R.string.copy_db_exception));
  10.   } else {
  11.   // 拷贝成功,更新数据库版本
  12.   try {
  13.   PackageInfo info = activity.getPackageManager()
  14.   .getPackageInfo(activity.getPackageName(), 0);
  15.   // 当前程序版本号,在AndroidManifest.xml中定义
  16.   int versionCode = info.versionCode;
  17.   Config.saveDbVer(versionCode);
  18.   Debug.log("拷贝成功" + result);
  19.   } catch (NameNotFoundException e) {
  20.   Debug.e(e);
  21.   }
  22.   }
  23.   } else {
  24.   // 数据库已存在的状况
  25.   if (dbUpdate.needUpdate() ) {
  26.   activity.showProgress("数据库升级中,请稍后", false);
  27.   new Thread() {
  28.   @Override
  29.   public void run() {
  30.   try {
  31.   Debug.log("update db");
  32.   dbUpdate.updateDb();
  33.   handler.sendEmptyMessage(0);
  34.   } catch (Exception e) {
  35.   Debug.e(e);
  36.   handler.sendEmptyMessage(-1);
  37.   }
  38.   };
  39.   }.start();
  40.   }
  41.   }
  42.   }
复制代码

  其中有几个要点:

  一、检测数据库文件是否已经存在。不存在则从raw文件夹复制数据库文件拷贝至SD卡中指定目录。

  二、数据库版本是根据应用的versionCode相同。拷贝数据库后,会把当前versionCode写入数据库的表中。

  Xml代码
  
  1. xmlns:android="http://schemas.android.com/apk/res/android"
  2.   package="com.xxx"
  3.   android:versionCode="2"
  4.   android:versionName="1.01.001">
  5.   versionCode在AndroidManifest.xml文件中。
复制代码

  在这种方案下,其实是由versionCode控制数据库版本,versionName控制程序版本。

  三、SD卡指定目录已经存在数据库文件的状况,则读取其中保存的数据库版本号,与versionCode对比,从而肯定是否须要升级数据库。代码以下:

  Java代码
  
  1. public boolean needUpdate() {
  2.   int currVer = Config.getDbVer();
  3.   return currVer < getAppVersion();
  4.   }
  5.   public int getAppVersion(){
  6.   try {
  7.   PackageInfo info = context.getPackageManager().getPackageInfo(
  8.   context.getPackageName(), 0);
  9.   // 当前程序版本号,在AndroidManifest.xml中定义
  10.   return info.versionCode;
  11.   } catch (NameNotFoundException e) {
  12.   Debug.e(e);
  13.   return 1;
  14.   }
  15.   }
复制代码

  三,升级数据库

  包括三个步骤:

  一、从程序中拷贝新数据库文件至SD卡指定目录,命名为temp.db。

  Java代码
 
  1.  String temp = SysConst.DB_FOLDER + "temp.db";
  2.   boolean s1 = FileOperator.write2Sdcard(context, R.raw.scpip_collection,temp);
复制代码

  二、分别获取两个数据源。

  Java代码
  
  1. //原数据库文件
  2.   BaseDao sd = new BaseDao();
  3.   //新数据库文件
  4.   BaseDao nd = new BaseDao(temp);
  5.   对于SQLite数据库来说,数据源就是数据库文件。BaseDao是本身封装的,关键在于要能够配置不一样的数据源
复制代码

  具体实现的相关代码以下:

  Java代码
  
  1. private String dbPath;
  2.   public BaseDao() {}
  3.   public BaseDao(String dbPath) {
  4.   this.dbPath = dbPath;
  5.   }
  6.   public SQLiteDatabase getDb() {
  7.   return SQLiteDatabase.openDatabase(SysConst.DB_PATH, null,
  8.   SQLiteDatabase.OPEN_READWRITE);
  9.   }
  10.   public SQLiteDatabase getDb(String dbPath) {
  11.   return SQLiteDatabase.openDatabase(dbPath, null,
  12.   SQLiteDatabase.OPEN_READWRITE);
  13.   }
复制代码

  这样就能够根据文件,获取不一样的SQLiteDatabase 对象。

  三、传输数据

  把原数据库中的数据查询出来,插入到新数据库中。

  Java代码
  
  1. public void transfer(BaseDao sd,BaseDao nd,Class cls) throws Exception{
  2.   List list = sd.find(cls, null);
  3.   nd.batchInsert(list);
  4.   }
复制代码

  这里有两个要点,

  第一,使用了自行封装的ORM。

  第二,使用了SQLite批量插入,增长写入效率。代码以下:

  Java代码
  
  1. @Override
  2.   public void batchInsert(List datas) throws Exception {
  3.   SQLiteDatabase dba = null;
  4.   if(dbPath == null){
  5.   dba = getDb();
  6.   } else {
  7.   dba = getDb(dbPath);
  8.   }
  9.   int size = datas.size();
  10.   try {
  11.   dba.beginTransaction();
  12.   for (int i = 0; i < size; i++) {
  13.   DaoHelper helper = new DaoHelper(datas.get(i));
  14.   String tableName = helper.getTableName();
  15.   String sql = "select 1 from " + tableName + " where "
  16.   + helper.getPkCol() + "=";
  17.   Object id = helper.getPkValue();
  18.   if (id instanceof String) {
  19.   sql = sql + "'" + id + "'";
  20.   } else {
  21.   sql = sql + id;
  22.   }
  23.   c = dba.rawQuery(sql, null);
  24.   if (c != null ? (c.getCount() == 1) : false) {
  25.   c.close();
  26.   continue;
  27.   }
  28.   if(c != null){
  29.   c.close();
  30.   }
  31.   // SqlArgs sa = helper.prepareInsert();
  32.   // dba.execSQL(sa.getSql(), sa.getArgs());
  33.   dba.insert(helper.getTableName(), "", helper.getInsertContent());
  34.   }
  35.   dba.setTransactionSuccessful();
  36.   dba.endTransaction();
  37.   } finally {
  38.   dba.close();
  39.   }
  40.   }
复制代码

  四、写入当前数据库版本,即从程序得到的versionCode。

  五、删除原数据库文件,重命名temp.db。

  完整过程以下:

  Java代码
 
  1.  public void updateDb() throws Exception {
  2.   //1,写入新的数据库文件,命名为temp
  3.   String temp = SysConst.DB_FOLDER + "temp.db";
  4.   boolean s1 = FileOperator.write2Sdcard(context, R.raw.scpip_collection,temp);
  5.   Debug.log("s1:" + s1);
  6.   if(s1) {
  7.   //原数据库文件
  8.   BaseDao sd = new BaseDao();
  9.   //新数据库文件
  10.   BaseDao nd = new BaseDao(temp);
  11.   //转移数据
  12.   //此处代码略
  13.   //删除原数据库文件,重命名临时数据库文件
  14.   if(FileOperator.delSdcardFile(SysConst.DB_PATH)){
  15.   File file = new File(temp);
  16.   file.renameTo(new File(SysConst.DB_PATH));
  17.   }
  18.   //此时更新数据库版本
  19.   Config.saveDbVer(getAppVersion());
  20.   }
  21.   }
复制代码

  至此,整个数据库升级完成。在保留原数据的基础上,获取了新的数据库结构。

  四,问题

  1,须要保留的原数据与新表结构不符。这个能够在程序中控制,建立新的OR映射。

  2,效率问题,若是须要保留的数据量很是大的状况下,是否会出现问题。这个是亟待解决的,目前我尚未想到解决办法。

  五,总结

  这个方案针对的状况是外部建立数据库文件,程序启动时从apk包将数据库文件拷贝进SD卡,从而达到对数据库文件的彻底控制。

  方案步骤:

  1,检测是否须要升级数据库文件。数据库文件版本是由versionCode控制。程序升级时,若是须要升级数据库,则要将versionCode+1。

  2,将新数据库文件(存在于apk中),写入SD卡。

  3,转移数据。

  4,删除原数据库文件,重命名新数据库文