標籤:orm 架構 opendroid
在上一篇部落格《打造android ORM架構opendroid(六)——級聯查詢》我們講了OpenDroid最後一塊功能查詢的實現原理。今天我們將進行OpenDroid一個重頭戲,也是本系列部落格的最後一篇——資料庫升級方案。
說道資料庫升級,我可是很頭疼的, 為什麼呢? 因為以前的項目中,根本沒有考慮資料庫升級方案的問題,就直接drop table了,這樣導致的結果就是“以前的資料都消失了”。額。。。 憑空消失確實不是很少的一件事,如果資料不重要還行,重要資料呢? 說消失就消失了? 使用者升級了一下軟體,結果資料全沒了。。。 那是多吊絲的一件事。
OpenDroid則提供了一種資料庫升級的方案,當然這種方案肯定不是完美的。 肯定還有更好的方案,如果你發現你有好的解決方案,請不吝賜教。
好,下面開始進入正題。首先說說我的方案的原理吧:其實很簡單,就是在drop table之前將資料查詢出來,並儲存到集合中,在建立新表後,嘗試去insert資料。原理的思路很簡單,以至於我一直認為這種方案太爛了, 可我沒有想到更好的結果方案,也就只能先這樣了。
大家都知道,android的SQLiteOpenHelper類中提供了一個抽象方法onUpgrade()來讓使用者靈活的定製資料庫升級方案, 最簡單的方法就是我之前提到直接drop table。既然upgrade的權利掌握在我們手中,那我們何不借onUpgrade()大幹一番呢?
先來看看代碼:
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { System.out.println("upgrade database"); upgrade(db); }
在onUpgrade裡除了一句列印,其實真正有用了就一句代碼,當然也是調用了我們自訂的一個方法,那麼我們就從upgrade()這個方法開始說起:
/** * 升級資料庫 * @param db 資料庫連結 */ private <T extends OpenDroid> void upgrade(SQLiteDatabase db) { try { XmlPullParser pullParser = Xml.newPullParser(); InputStream inputStream = DroidApplication.sContext.getAssets().open("open_droid.xml"); pullParser.setInput(inputStream, "utf-8"); int type = pullParser.getEventType(); while(type != XmlPullParser.END_DOCUMENT) { if(type == XmlPullParser.START_TAG) { // 擷取mapping if(pullParser.getName().equals(OpenDroidHelper.TAG_MAPPING)) { dumpData(db, pullParser); } } type = pullParser.next(); } } catch (Exception e) { e.printStackTrace(); } // 執行建立資料庫 onCreate(db); }
7~9行可以看出我們準備去解析open_droid.xml檔案了,和我們平時解析一樣,使用一個while迴圈,觀察while迴圈,我們在15~17行擷取到了有用的資訊,如果當前的tag是mapping的或,我們又去調用了dumpData,這裡面XmlPullParser會作為第二個參數傳遞過去。
方法的最後,我們直接調用了重載的onCreate方法去建立新表,當然還有資料的恢複。這個我們稍後分析,接下來我們來看看dumpData方法。
/** * 將資料庫中的資料轉儲到程式中 * @param db 資料庫連接 * @param pullParser * @throws Exception */ private <T extends OpenDroid> void dumpData(SQLiteDatabase db, XmlPullParser pullParser) throws Exception { Class<OpenDroid> klass = (Class<OpenDroid>) Class.forName(pullParser.getAttributeValue(null, "class")); String tableName = klass.getSimpleName(); // 表名 Cursor cursor = db.rawQuery("select * from " + tableName, null); T t; Method m; String methodName; String columnName; // 遍曆遊標 for(cursor.moveToFirst();!cursor.isAfterLast();cursor.moveToNext()) { t = (T) klass.newInstance(); // 通過反射進行執行個體化 final int columnCount = cursor.getColumnCount(); for(int i=0;i<columnCount;i++) { columnName = cursor.getColumnName(i); // 擷取欄位名 // try一下,如果沒有該欄位對應的方法,則消化異常,並繼續 try { switch (cursor.getType(i)) { case Cursor.FIELD_TYPE_INTEGER: methodName = columnName.equals("_id") ? "setId" : CRUD.getMethodName(cursor.getColumnName(i)); m = klass.getMethod(methodName, int.class); // 反射出方法 m.invoke(t, cursor.getInt(i)); // 執行方法 break; case Cursor.FIELD_TYPE_FLOAT: methodName = CRUD.getMethodName(cursor.getColumnName(i)); m = klass.getMethod(methodName, float.class); m.invoke(t, cursor.getFloat(i)); break; default: methodName = CRUD.getMethodName(cursor.getColumnName(i)); m = klass.getMethod(methodName, String.class); m.invoke(t, cursor.getString(i)); break; } } catch (Exception e) { e.printStackTrace(); } } mOldData.add(t); } cursor.close(); db.execSQL("drop table if exists " + tableName); // 刪除舊的資料庫 }
這個方法很長,而且也很關鍵,我們的資料庫升級方案將在這裡終結。
第9行,我們通過Class.forName擷取了一個Class, 是根據什麼映射呢?來看一下我們open_droid.xml檔案就一目瞭然。
<open-droid> <version value="6" /> <name value="school" /> <mapping class="org.loader.opendroid.Student" /> <mapping class="org.loader.opendroid.Grade" /> </open-droid>
這這個xml中,我們就是要通過org.loader.opendroid.Student來映射出一個類。
第10行,我們擷取了該類的類名,當然也就是我們要操作的表名了,唉? 為什麼就一個表呢? 仔細看看這個方法是在哪調用的,我們是在一個迴圈中調用了,也就是在迴圈中去遍曆xml節點,每次擷取到mapping節點,都來調用一下這個方法。
11行,我們執行一段select語句,將現在表中所有的資料查詢出來,那查詢出來的資料我們怎麼處理呢?
要回答這個問題,我們就得去下面的for迴圈中找答案。
在for迴圈中,20行,通過反射執行個體化了上面那個類,為什麼要在迴圈中執行個體化呢?因為每行資料我們都需要用一個對象來儲存。
21行,擷取了當前行所有列的個數。
接下來有一個for迴圈,這個迴圈我們是迴圈的每一行的列,在迴圈中去取每一列的資料。
26行,進入一個switch語句,依照慣例,我們只去分析一個case語句。
在第一個case中,如果改列的欄位是一個integer類型,28行,我們和之前講過的一樣去拼裝一個setter,當然如果是_id的話,我們就直接定義為setId了。
30行,反射出這個方法,等待下面去執行。
當然31行我們就要去執行這個方法了,我們都知道setter方法是需要參數的,參數當然就是我們查詢出來的當前列的資料了。
48行,我們將這個對象的實力放入一個集合中。
當查詢完當前表,這個表就沒用了,因為我們已經把資料都儲存起來了,所以在51行,將該表刪除。
至此,我們就把資料從舊版本的資料庫中全部查詢出來了。接下來我們回到onCreate方法中來看看新表是如果建立的,並且資料是如何恢複的。
@Override public void onCreate(SQLiteDatabase db) { for(String sql : OpenDroidHelper.getDBInfoBean().getSqls()) { db.execSQL(sql); } // 還原資料 if(!mOldData.isEmpty()) { for(OpenDroid item : mOldData) { item.save(db); } } }
前面幾行代碼,我們在《打造android ORM架構opendroid(二)——自動建立資料庫》 已經講解過,這裡就不重複了,我們重點來看看在那篇部落格中省略的幾行代碼,也正是這幾行代碼,實現了舊資料向新表中的轉移。
8行,先去判斷mOldData是否為空白的集合,因為onCreate方法並不是只有在資料庫升級的時候才去執行。
接下來遍曆整個集合,並且調用每個item的save方法將資料儲存到新表中,當然這裡我們重用了OpenDroid類中的save方法,因為都是insert嘛。從這裡我們也可以看出這個mOldData集合的泛型肯定是OpenDroid。
private ArrayList<OpenDroid> mOldData = new ArrayList<OpenDroid>();
好了,至此,我們opendroid提供的一個簡單的資料庫升級方案就執行完了,而且我們的opendroid也介紹的差不多了,剩下的一點東西都是輔助性的東西。哦,對了,這裡還要提一點:細心的朋友可能已經發現了,opendroid在操作完資料庫並沒有預設的關閉掉資料庫,而是蛋疼的提供了open和release兩個方法,不信可以看代碼:
/** * 開啟資料庫 */ public static void open() { if(sSqliteDatabase == null) { sSqliteDatabase = sOpenHelper.getWritableDatabase(); } } /** * 釋放資料庫 */ public static void release() { if(sSqliteDatabase != null && sSqliteDatabase.isOpen()) { sSqliteDatabase.close(); } sSqliteDatabase = null; }
這是為什麼呢? 其實剛開始我是做了預設關閉了,可就是在寫到資料庫升級恢複資料的時候,因為save是在一個迴圈中執行了,因此,可能在很短的時間內多次開啟/關閉資料庫,這樣做會消耗很大的效能,所以android會拋出一個異常,在一氣之下,我就將代碼改造成了這種方式,如果有大神有更好的解決方案,請賜教哈。
好了,至此我們opendroid系列部落格也就尾聲了,當然,做出一個orm架構本身並不重要,重要的是學會如何去做一個orm架構,別人能做的事,我們為什麼就不能呢?對吧,作為一個程式員,我們要努力去做一個“創造者”,而不是簡單停留在一個“使用者”上。
最後是opendroid的開源地址:http://git.oschina.net/qibin/OpenDroid
打造android ORM架構opendroid(七)——資料庫升級方案