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默認是空的,這時候舊數據可能相當於不起作用了,雖然可以通過設置默認值,但其需要根據具體業務場景進行設置,因此就失去其靈活性了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM