1. 程式人生 > >Android中當資料庫需要更新時我們該怎麼辦

Android中當資料庫需要更新時我們該怎麼辦

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow

也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!

               

問題:Android資料庫更新並保留原來的資料如何實現

Andoird的SQLiteOpenHelper類中有一個onUpgrade方法。幫助文件中只是說當資料庫升級時該方法被觸發。經過實踐,解決了我一連串的

疑問:
1. 幫助文件裡說的“資料庫升級”是指什麼?
你開發了一個應用,當前是1.0版本。該程式用到了資料庫。到1.1版本時,你在資料庫的某個表中增加了一個欄位。那麼軟體1.0版本用的資料庫在軟體1.1版本就要被升級了(當然這裡的升級包括兩個概念,一個是應用升級還有就是資料庫升級)

關於應用升級我們知道直接在AndroidManifest.xml檔案中修改即可。

關於資料庫升級我們就需要在程式碼中修改:

private static final String DBNAME = "ads.db";private static final int VERSION = 1;public
DBOpenHelper(Context context)
super(context, DBNAME, null, VERSION);}
我們在使用資料庫的時候,都會自定義一個Helper類,需要實現SQLiteOpenHelper類(是個抽象類),然後實現他的onCreate和onUpgrade方法。上面的一段程式碼片段就是我們自定義的Helper類中的,這裡我們會看到在構造方法中,我們會呼叫父類(就是SQLiteOpenHelper類的構造方法),這裡需要傳遞Context變數、資料庫名稱、以及資料庫的版本號(第三個引數是一個工廠類,可以傳遞null)。所以我們就可以在這個地方進行資料庫的升級了,當然我們一般第一次把資料庫的版本號設定成1,以後遞增即可,這個的原因,後面在分析原始碼的時候會提到。

2. 資料庫升級應該注意什麼?

軟體的1.0版本升級到1.1版本時,資料庫中老的資料不能丟。那麼在1.1版本的應用中就要有地方能夠檢測出來新的資料庫與老的資料庫不相容。並且能夠有辦法把1.0應用中的資料庫升級到1.1應用時能夠使用的資料庫。換句話說,要在1.0應用的資料庫中的那個表中增加那個欄位,並賦予這個欄位預設值。


3. 應用如何知道資料庫需要升級?
SQLiteOpenHelper類的建構函式有一個引數是int version,它的意思就是指資料庫版本號。比如在應用1.0版本中,我們使用
SQLiteOpenHelper訪問資料庫時,該引數為1,那麼資料庫版本號1就會寫在我們的資料庫中。到了1.1版本,我們的資料庫需要發生變化,那麼我們1.1版本的程式中就要使用一個大於1的整數來構造SQLiteOpenHelper類,用於訪問新的資料庫,比如2。當我們的1.1新程式讀取1.0版本的老資料庫時,就發現老資料庫裡儲存的資料庫版本是1,而我們新程式訪問它時填的版本號為2,系統就知道資料庫需要升級。


4. 何時觸發資料庫升級?如何升級?

當系統在構造SQLiteOpenHelper類的物件時,如果發現版本號不一樣,就會自動呼叫onUpgrade函式,讓你在這裡對資料庫進行升級。根據上述場景,在這個函式中把老版本資料庫的相應表中增加欄位,並給每條記錄增加預設值即可。

新版本號和老版本號都會作為onUpgrade函式的引數傳進來,便於開發者知道資料庫應該從哪個版本升級到哪個版本。

升級完成後,資料庫會自動儲存最新的版本號為當前資料庫版本號。


下面就從原始碼的角度分析一下,執行流程:

首先來看一下SQLiteOpenHelper.java類

    public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version) {        this(context, name, factory, version, null);    }

我們看到了,這個構造方法,就是我們在子類中呼叫的,第三個引數是一個遊標工廠類,可以傳遞null。再看一下他的其他構造方法:

    public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version,            DatabaseErrorHandler errorHandler) {        if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version);        mContext = context;        mName = name;        mFactory = factory;        mNewVersion = version;        mErrorHandler = errorHandler;    }

這裡我們會看到一個資訊,就是會判斷版本號,如果版本號小於1的話直接拋異常了,所以我們一般將版本號設定成1。構造結束了,下面來看一下他的onCreate和onUpgrade方法:

    public abstract void onCreate(SQLiteDatabase db);    public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);

這兩個方法都是抽象的,需要子類去實現他們。

好吧,那下面我們該看哪個方法呢?當然是看我們使用到的方法了。我們一般在使用資料庫的時候會用到的程式碼:

DBOpenHelper openHelper = new DBOpenHelper(this);SQLiteDatabase db = openHelper.getWritableDatabase();

初始化自定義的Helper類,然後獲取一個數據庫物件SQLiteDatabase。那麼我們就來看看你getWritableDatabase方法:

    public SQLiteDatabase getWritableDatabase() {        synchronized (this) {            return getDatabaseLocked(true);        }    }

這裡用到了同步機制了,接著看getDatabaseLocked方法:

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) {                        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) {                    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();                    db = SQLiteDatabase.openDatabase(path, mFactory,                            SQLiteDatabase.OPEN_READONLY, mErrorHandler);                }            }            onConfigure(db);            final int version = db.getVersion();            if (version != mNewVersion) {                if (db.isReadOnly()) {                    throw new SQLiteException("Can't upgrade read-only database from version " +                            db.getVersion() + " to " + mNewVersion + ": " + mName);                }                db.beginTransaction();                try {                    if (version == 0) {                        onCreate(db);                    } else {                        if (version > mNewVersion) {                            onDowngrade(db, version, mNewVersion);                        } else {                            onUpgrade(db, version, mNewVersion);                        }                    }                    db.setVersion(mNewVersion);                    db.setTransactionSuccessful();                } finally {                    db.endTransaction();                }            }            onOpen(db);            if (db.isReadOnly()) {                Log.w(TAG, "Opened " + mName + " in read-only mode");            }            mDatabase = db;            return db;        } finally {            mIsInitializing = false;            if (db != null && db != mDatabase) {                db.close();            }        }    }

這個方法的東西就有點多了,貌似也是最核心的部分。

首先我們看到一個欄位mDatabase,是個SQLiteDatabase型別的

private SQLiteDatabase mDatabase;

下面來看一下開始部分的程式碼:

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

如果mDatabase不為null的話,然後在判斷如果這個資料庫是否是關閉的狀態,如果關閉了就將其設定null。如果沒有關閉,就判斷他的狀態是可讀還是可寫的狀態,然後返回一個例項即可.


繼續下面的程式碼:

SQLiteDatabase db = mDatabase;try { mIsInitializing = trueif (db != null) {  if (writable && db.isReadOnly()) {   db.reopenReadWrite();  } } else if (mName == null) {  db = SQLiteDatabase.create(null); } else {  try {   if (DEBUG_STRICT_READONLY && !writable) {    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) {   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();       db = SQLiteDatabase.openDatabase(path, mFactory,         SQLiteDatabase.OPEN_READONLY, mErrorHandler);  } }

這段程式碼主要是初始化一個SQLiteDatabase物件。


繼續:

final int version = db.getVersion();if (version != mNewVersion) { if (db.isReadOnly()) {  throw new SQLiteException("Can't upgrade read-only database from version " +    db.getVersion() + " to " + mNewVersion + ": " + mName); } db.beginTransaction(); try {  if (version == 0) {   onCreate(db);  } else {   if (version > mNewVersion) {    onDowngrade(db, version, mNewVersion);   } else {    onUpgrade(db, version, mNewVersion);   }  }  db.setVersion(mNewVersion);  db.setTransactionSuccessful(); } finally {  db.endTransaction(); }}

這段程式碼就是我們這次的主要研究物件,也會解決我們的很多疑問。程式碼的邏輯很簡單:

首先他獲取到當前資料庫的版本號,這裡的db就是之前程式碼中建立的一個SQLiteDatabase物件,然後呼叫它的getVersion方法獲取版本號,檢視getVersion的原始碼:

    public int getVersion() {        return ((Long) DatabaseUtils.longForQuery(this, "PRAGMA user_version;", null)).intValue();    }


這裡看到是一個DatabaseUtils類的longForQuery方法,繼續找到這個方法的原始碼:

    public static long longForQuery(SQLiteDatabase db, String query, String[] selectionArgs) {        SQLiteStatement prog = db.compileStatement(query);        try {            return longForQuery(prog, selectionArgs);        } finally {            prog.close();        }    }

找到這個方法了,其實他內部很簡單,就是執行一個sql語句,sql語句為:

PRAGMA user_version;
這裡查詢的結果是long型別的,所以我們在定義資料庫的版本的時候一般是整數,遞增也是整數。

那麼當我們首次建立資料庫的時候他的版本值是多少呢?其實這個可以猜的。可能為0。那麼我們就來做個驗證吧。這裡的驗證不是用程式碼的方式了,而是藉助於一個軟體:Sqlite Expert

這個軟體用起來還是比較簡單的。我們新建一個數據庫,然後執行上面的SQL語句,結果如下圖:


果然第一次得到的版本號是0,好的下面繼續來看一下之前的程式碼分析,之前分析到了,獲取資料庫的版本號,然後就是和之前的版本進行比較,如果不相等


這裡又會做一個判斷,判斷當前的資料庫是否為只讀的,如果是隻讀的話,是不能進行後續的更新操作,拋個異常。

if (db.isReadOnly()) { throw new SQLiteException("Can't upgrade read-only database from version " +   db.getVersion() + " to " + mNewVersion + ": " + mName);}

如果不是隻讀的話,繼續下面的程式碼:

db.beginTransaction();tryif (version == 0) {  onCreate(db); } else {  if (version > mNewVersion) {   onDowngrade(db, version, mNewVersion);  } else {   onUpgrade(db, version, mNewVersion);  } } db.setVersion(mNewVersion); db.setTransactionSuccessful();} finally { db.endTransaction();}

開啟一個事務。這裡會判斷如果獲取到的資料庫的版本為0,那麼就執行onCreate方法,如果版本號有增加就會執行onUpgrade方法,如果版本號有遞減的話,就會執行onDowngrade方法,拋異常了。

public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {        throw new SQLiteException("Can't downgrade database from version " +                oldVersion + " to " + newVersion);}

然後設定資料庫的版本為最新的值。結束事務。


這裡在使用的時候遇到一個問題,就是我在onUpgrade方法中呼叫了onCreate方法,但是此時onCreate方法會報異常,當然我們將其捕獲了(在外面使用Helper的時候捕獲的),但是每次開啟應用的時候,onUpgrade方法都會執行。當時一直在找原因。沒頭緒呀。

其實這裡就可以找到原因,因為當onUpgrade方法報異常(因為在onUpgrade方法中呼叫了onCreate方法,當onCreate方法報異常時,onUpgrade方法沒有捕獲到這個異常就還會報異常)之後,後續程式碼就不執行了,那麼下面的設定資料庫最新版本號的程式碼也不會執行了,所以每次開啟app的時候,會進行版本號的比對,結果還是會執行onUpgrade方法,這個方法還是會報異常,所以會出現每次開啟app的時候onUpgrade方法都會執行一次。下面來看一下

package com.sohu.sqlitedemo;import android.content.Context;import android.database.sqlite.SQLiteDatabase;import android.database.sqlite.SQLiteDatabase.CursorFactory;import android.database.sqlite.SQLiteOpenHelper;import android.util.Log;import android.widget.Toast;public class DBOpenHelper extends SQLiteOpenHelper private static final String DBNAME = "ads.db"private static final int VERSION = 1public DBOpenHelper(Context context) {  super(context, DBNAME, null, VERSION); } @Override public void onCreate(SQLiteDatabase db) {  Log.i("DEMO","oldVersion,onCreate()");  db.execSQL("CREATE TABLE IF NOT EXISTS offlineBanner("//    + "id integer primary key autoincrement," //    + "vid VARCHAR(100),"//     //+ "adsequences integer,"    //+ "isofflineads integer,"    + "Impression VARCHAR(2000),"//    + "Duration VARCHAR(50)," //    + "ClickThrough VARCHAR(500),"//     + "ClickTracking VARCHAR(500)," //    + "MediaFile VARCHAR(500)," //    + "creativeView VARCHAR(500),"//     + "start VARCHAR(500)," //    + "firstQuartile VARCHAR(500),"//     + "midpoint VARCHAR(500)," //    + "thirdQuartile VARCHAR(500),"//     + "complete VARCHAR(500)," //    + "sdkTracking VARCHAR(2000),"//     + "sdkClick VARCHAR(2000)," //    + "time VARCHAR(50));");//  //這行程式碼會報異常  db.execSQL("CREATE TABLE offlinePause("//    + "id integer primary key autoincrement,"//     + "vid VARCHAR(100)," //    + "Impression VARCHAR(2000)," //    + "StaticResource VARCHAR(500),"//     + "NonLinearClickThrough VARCHAR(500),"//     + "sdkTracking VARCHAR(2000)," //    + "sdkClick VARCHAR(2000)," //    + "time VARCHAR(50));");//    db.execSQL("CREATE TABLE IF NOT EXISTS download_url ("//    + "id integer primary key autoincrement,"//     + "url varchar(500),"//    + "status integer,"//    + "length integer)");// } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {  Log.i("DEMO","oldVersion=" + oldVersion + ",newVersion" + newVersion);  if (oldVersion != newVersion) {   db.execSQL("ALTER TABLE offlineBanner ADD COLUMN adsequence integer;");   db.execSQL("ALTER TABLE offlineBanner ADD COLUMN isofflinead integer;");   db.execSQL("DROP TABLE IF EXISTS download_url");  }  //執行方法會報異常  onCreate(db); }}


報異常的原因很簡單。當我需要更新的時候,就會執行onUpgrade方法,這裡又執行了onCreate方法,在建立pause表的時候會報異常,因為這個表已經存在了。


當我們把程式碼外面加上try...catch的時候:

try//開始使用資料庫 DBOpenHelper openHelper = new DBOpenHelper(this); SQLiteDatabase db = openHelper.getWritableDatabase(); db.close();}catch (Exception e){}

問題會解決,但是onUpgrade方法就會執行多次:


所以還是要解決一下onCreate方法中的異常問題,那個異常就是因為表存在了,所以建立表的時候需要判斷表是否已經存在了。這樣就可以了。

上面從原始碼的角度瞭解了原理,下面來看一下例項:

做Android應用,不可避免的會與SQLite打交道。隨著應用的不斷升級,原有的資料庫結構可能已經不再適應新的功能,這時候,就需要對SQLite資料庫的結構進行升級了。 SQLite提供了ALTER TABLE命令,允許使用者重新命名或新增新的欄位到已有表中,但是不能從表中刪除欄位。

並且只能在表的末尾新增欄位,比如,為 Subscription新增兩個欄位:
ALTER TABLE Subscription ADD COLUMN Activation BLOB;ALTER TABLE Subscription ADD COLUMN Key BLOB;

另外,如果遇到複雜的修改操作,比如在修改的同時,需要進行資料的轉移,那麼可以採取在一個事務中執行如下語句來實現修改表的需求。

1. 將表名改為臨時表
 ALTER TABLE Subscription RENAME TO __temp__Subscription;

2. 建立新表
CREATE TABLE Subscription (OrderId VARCHAR(32) PRIMARY KEY ,UserName VARCHAR(32) NOT NULL ,ProductId VARCHAR(16) NOT NULL);

3. 匯入資料 
 
INSERT INTO Subscription SELECT OrderId, “”, ProductId FROM __temp__Subscription;
或者  
INSERT INTO Subscription() SELECT OrderId, “”, ProductId FROM __temp__Subscription;
* 注意 雙引號”” 是用來補充原來不存在的資料的
4. 刪除臨時表 
 
DROP TABLE __temp__Subscription;
通過以上四個步驟,就可以完成舊資料庫結構向新資料庫結構的遷移,並且其中還可以保證資料不會應為升級而流失。
當然,如果遇到減少欄位的情況,也可以通過建立臨時表的方式來實現。


Android應用程式更新的時候如果資料庫修改了欄位需要更新資料庫,並且保留原來的資料庫資料:
這是原有的資料庫表

CREATE_BOOK = "create table book(bookId integer primarykey,bookName text);";
然後我們增加一個欄位:
CREATE_BOOK = "create table book(bookId integer primarykey,bookName text,bookContent text);";
首先我們需要把原來的資料庫表重新命名一下
CREATE_TEMP_BOOK = "alter table book rename to _temp_book";
然後把備份表中的資料copy到新建立的資料庫表中
INSERT_DATA = "insert into book select *,' ' from _temp_book";(注意' '是為新加的欄位插入預設值的必須加上,否則就會出錯)

然後我們把備份表幹掉就行啦。
DROP_BOOK = "drop table _temp_book";

然後把資料庫的版本後修改一下,再次建立資料庫操作物件的時候就會自動更新(注:更新的時候第一個建立的操作資料的物件必須是可寫的,也就是通過這個方法getWritableDatabase()獲取的資料庫操作物件)

然後在onUpgrade()方法中執行上述sql語句就OK

public class DBservice extends SQLiteOpenHelperprivate String CREATE_BOOK = "create table book(bookId integer primarykey,bookName text);"private String CREATE_TEMP_BOOK = "alter table book rename to _temp_book"private String INSERT_DATA = "insert into book select *,'' from _temp_book"private String DROP_BOOK = "drop table _temp_book";  public DBservice(Context context, String name, CursorFactory factory,int version) {  super(context, name, factory, version); }  @Override public void onCreate(SQLiteDatabase db) {  db.execSQL(CREATE_BOOK); }  @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {  switch (newVersion) {  case 2:   db.execSQL(CREATE_TEMP_BOOK);   db.execSQL(CREATE_BOOK);   db.execSQL(INSERT_DATA);   db.execSQL(DROP_BOOK);   break;  } }}


總結:這次遇到的問題,開始的時候不知道怎麼解決,那我們就繼續去看那個操蛋的程式碼吧,這裡的原始碼其實不難的~~,但是我們要養成看原始碼的習慣。

《Android應用安全防護和逆向分析》

點選立即購買:京東  天貓


更多內容:點選這裡

關注微信公眾號,最新技術乾貨實時推送

編碼美麗技術圈 微信掃一掃進入我的"技術圈"世界

掃一掃加小編微信
新增時請註明:“編碼美麗”非常感謝!            

給我老師的人工智慧教程打call!http://blog.csdn.net/jiangjunshow

這裡寫圖片描述