Android SQLite的ORM介面實現(一)---findAll和find的實現
最近在看Android的ORM資料庫架構LitePal,就想到可以利用原生的SQLite來實現和LitePal類似的ORM介面實現。 LitePal有一個介面是這樣的: List<Status> statuses = DataSupport.findAll(Status.class); 指定什麼類型,就能擷取到該類型的資料集合。 這樣是很方便,於是想著自己不看它們的實現,自己搞一個出來。 首先想到的就是利用反射和泛型。 利用反射有一個比較好的方式就是註解,讀取註解就知道哪些屬性是要被賦值的,但現在我還不想使用註解,那該怎麼辦呢? 我想到了利用反射來調用set方法完成賦值。 首先我們要知道什麼欄位需要賦值,反射是可以擷取到欄位,但可惜的是,它無法確定屬性的名稱和類型,原生的SQLite操作是要知道列名的。 反射是可以知道屬性的名字的:Field[] fields = clazz.getDeclaredFields();for (Field field : fields) { Log.e("DatabaseStore", field.getName());} Java的Class API有getFields和getDeclaredFields兩個方法,前者是用來擷取public欄位的,後者是用來擷取所有聲明的欄位的,顯然必須使用後者,而且注意的是,因為擷取到的欄位是所有聲明的欄位,所以絕對有可能擷取到不需要的欄位。 但光知道屬性的名字還是不夠的,Android的SQLite需要知道自己要擷取到的是什麼類型:cursor.getString(cursor.getColumnIndex("name")); 幸運的是,是可以擷取到的: for (Field field : fields) { Type type = field.getGenericType(); Log.e("DatabaseStore", type.toString());} 但如何知道哪些屬性是要被賦值的呢? 在代碼約束上,我們是可以要求model的所有屬性都是要被賦值的,沒有道理一個model出現的屬性竟然是不需要被賦值的,但實現上,我們還是假設有這樣的可能。 這就需要擷取到setter,只要有setter,就說明它是需要被賦值的: List<Method> setMethods = new ArrayList<Method>(); for (Method method : allMethods) { String name = method.getName(); if (name.contains("set") && !name.equals("offset")) { setMethods.add(method); continue; } } 這就要求我們所有的屬性的setter前面都必須帶有set關鍵字,這同樣也是種代碼約束。 既然同樣都是代碼約束,為什麼不能直接就是要求屬性必須都是要被賦值的呢? 很可惜的是,有可能這個model是需要被序列化的,而序列化有可能會有一個序列ID,序列ID是不需要被賦值的,但又是有可能存在於model中的。 比起這個,只要我們利用編輯器自動產生的setter,是一定會有set關鍵字的,所以,這種約束更加簡單。 接著我們的操作就很簡單了:判斷Field的名稱數組中的元素是否有對應的setter,如果有,就從Field的類型數組中取出該屬性的類型,然後判斷該類型屬於哪種類型,就去表中取出對應的值。 Cursor cursor = Connector.getDatabase().query(clazz.getSimpleName(), null, null, null, null, null, null);//查詢並獲得遊標 List<T> list = new ArrayList<T>(); Constructor<?> constructor = findBestSuitConstructor(clazz); while (cursor.moveToNext()) { T data = null; try { data = (T) constructor .newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } for (Method method : setMethods) { String name = method.getName(); String valueName = name.substring(3).substring(0, 1).toLowerCase() + name.substring(4); String type = null; int index = 0; if (fieldNames.contains(valueName)) { index = fieldNames.indexOf(valueName); type = fields[index].getGenericType().toString(); } Object value = new Object(); if (type != null) { if (type.contains("String")) { value = cursor.getString(cursor.getColumnIndex(valueName.toLowerCase())); } else if (type.equals("int")) { value = cursor.getInt(cursor.getColumnIndex(valueName.toLowerCase())); } else if (type.equals("double")) { value = cursor.getDouble(cursor.getColumnIndex(valueName.toLowerCase())); } else if (type.equals("float")) { value = cursor.getFloat(cursor.getColumnIndex(valueName.toLowerCase())); } else if (type.equals("boolean")) { value = cursor.getInt(cursor.getColumnIndex(valueName.toLowerCase())) == 1 ? true : false; } else if (type.equals("long")) { value = cursor.getLong(cursor.getColumnIndex(valueName.toLowerCase())); } else if (type.equals("short")) { value = cursor.getShort(cursor.getColumnIndex(valueName.toLowerCase())); } try { fields[index].setAccessible(true); fields[index].set(data, value); } catch (IllegalAccessException e) { Log.e("data", e.toString()); } } } list.add(data); } cursor.close(); 為了保證通用性,使用了泛型,但這裡有個小小的問題需要解決,就是如何new一個T? 這不是開玩笑的,因為T是無法new的,所以還是需要通過反射來完成。 通過反射來擷取構造器是必須的,但構造器有可能是有很多的,如何擷取到最佳的構造器還是個問題。 什麼是最佳構造器? 實際上,model的構造器基本上應該是無參構造器,但以防萬一,我們還是需要通過一個比較: protected Constructor<?> findBestSuitConstructor(Class<?> modelClass) { Constructor<?> finalConstructor = null; Constructor<?>[] constructors = modelClass.getConstructors(); for (Constructor<?> constructor : constructors) { if (finalConstructor == null) { finalConstructor = constructor; } else { int finalParamLength = finalConstructor.getParameterTypes().length; int newParamLength = constructor.getParameterTypes().length; if (newParamLength < finalParamLength) { finalConstructor = constructor; } } } finalConstructor.setAccessible(true); return finalConstructor; } 誰的參數最少,誰就是最佳構造器,0當然是最少的。 到了這裡,我們基本上就實現了一個擁有和LitePal的API一樣但內在實現卻是原生方法的資料庫介面方法了: List<Status> newData = DatabaseStore.getInstance().findAll(Status.class); LitePal當然會提供條件查詢的介面,也就是所謂的模糊查詢。 模糊查詢的基本結構如下:SELECT 欄位 FROM 表 WHERE 某欄位 Like 條件 其中,條件有四種匹配模式。 1.%,表示任意0個或更多字元,可匹配任意類型和長度的字元,有些情況下若是中文,就得使用%%表示。SELECT * FROM [user] WHERE u_name LIKE '%三%' 會把u_name中有“三”的記錄找出來。 可以用and條件來增加更多的條件: SELECT * FROM [user] WHERE u_name LIKE '%三%' AND u_name LIKE '%貓%' 這樣能夠找出u_name中的“三腳貓”的記錄,但無法找到“張貓三”的記錄。 2._,表示任意單個字元,匹配單個任一字元,用來限制運算式的字元長度語句: SELECT * FROM [user] WHERE u_name LIKE '_三_' 這樣只能找出“張三貓”這樣中間是“三”的記錄。 SELECT * FROM [user] WHERE u_name LIKE '三__'; 這樣是找到“三腳貓”這樣“三”放在開頭的三個單詞的記錄。 3.[],表示括弧內所列字元中的一個,指定一個字元,字串,或者範圍,要求匹配對象為它們中的任一個。SELECT * FROM [user] WHERE u_name LIKE '[張李王]三' 這樣是找到“張三”,“李三”或者“王三”的記錄。 如 [ ] 內有一系列字元(01234、abcde之類的),則可略寫為“0-4“,“a-e”:SELECT * FROM [user] WHERE u_name LIKE '老[1-9]' 這將找出”老1“,”老2“。。。等記錄。 4.[^],表示不在括弧所列之內的單個字元,其取值和[]相同,但它要求所匹配對象為指定字元以外的任一個字元。SELECT * FROM [user] WHERE u_name LIKE '[^張李王]三' 這樣找到的記錄就是排除”張三“,”李三“或者”王三“的其他記錄。 5.查詢內容包含萬用字元。 如果我們查特殊字元,如”%“,“_"等,一般程式是需要用"/"括起來,但SQL中是用"[]"。 知道了這些基本的知識後,我們就可以開始看LitePal的介面是怎樣的: List<Status> myStatus = DataSupport.where("text=?", "我好").find(Status.class); 這樣的介面比較簡單,並且允許鏈式調用,形式上更加簡潔。 要想實現這個,倒也不難,我們暫時就簡單的用一個condition的字串表示要查詢的條件,然後提供一個where方法實現where查詢的拼接,暫時就只是單個條件:private String conditionStr;public DatabaseStore where(String key, String value) { conditionStr = " where " + key + " like '%" + value + "%'"; return store;} 為了實現鏈式調用,返回DatabaseStore是必須的。 接下來就非常簡單了,只要拼接完整的SQL語句,然後執行就可以了: public <T> List<T> find(Class<T> clazz) { String sql = "SELECT * FROM " + clazz.getSimpleName().toLowerCase() + conditionStr; Cursor cursor = Connector.getDatabase().rawQuery(sql, null); Field[] fields = clazz.getDeclaredFields(); List<String> fieldNames = new ArrayList<String>(); for (Field field : fields) { fieldNames.add(field.getName()); } List<Method> setMethods = getSetMethods(clazz); List<T> list = getList(clazz, cursor, setMethods, fieldNames, fields); cursor.close(); conditionStr = ""; return list;} 複製代碼 getSetMethods方法就是上面擷取setter的代碼的封裝,而getList方法就是上面產生指定類型對象的List的代碼的封裝。 這樣我們的介面方法的調用就是這樣的:List<Status> data = DatabaseStore.getInstance().where("text", "我好").find(Status.class); 無論是LitePal還是我們自己的實現,where都必須放在find前面。 這裡倒有一個小貼士可以說說,就是擷取資料庫所有表名的操作。 由於底層我們還是使用LitePal來建表,而LitePal的建表非常簡單,就是在assets檔案夾下面放一個litepal.xml檔案: <?xml version="1.0" encoding="utf-8"?><litepal> <!-- 資料庫名稱 --> <dbname value="xxx.db"></dbname> <!-- 資料庫版本 --> <version value="1"></version> <!-- 資料庫表 --> <list> <mapping class="com.example.pc.model.Status"></mapping> </list></litepal> 但表名具體到底是啥呢? 為了確認一下,我們可以查詢資料庫中所有的表的名字:Cursor cursor = Connector.getDatabase().rawQuery("select name from sqlite_master where type='table' order by name", null);while (cursor.moveToNext()) { //遍曆出表名 String name = cursor.getString(0); Log.e("DatabaseStore", name);} 每一個SQLite的資料庫中都有一個sqlite_master的表,這個表的結構如下: CREATE TABLE sqlite_master ( type TEXT, name TEXT, tbl_name TEXT, rootpage INTEGER, sql TEXT); 對於表來說,type欄位是”table“,name欄位是表的名字,而索引,type就是”index“,name是索引的名字,tbl_name則是該索引所屬的表的名字。 不管是表還是索引,sql欄位是原先用CREATE TABLE或者CREATE INDEX語句建立它們時的命令文本,對於自動建立的索引,sql欄位為NULL。 sqlite_master表示唯讀,它的更新只能通過CREATE TABLE,CREATE INDEX,DROP TABLE或者DROP INDEX命令自動更新。 暫存資料表不會出現在sqlite_master中,暫存資料表及其索引和觸發器是存放在另外一個叫sqlite_temp_master的表中,如果想要查詢包括暫存資料表在內的所有的表的列表,就需要這樣寫:SELECT name FROM(SELECT * FROM sqlite_master UNION ALLSELECT * FROM sqlite_temp_master)WHERE type=’table’ORDER BY name LitePal還可以對結果進行排序: List<Status> myStatus = DataSupport.where("text=?", "我好").order("updatetime").find(Status.class); 這個也是很簡單就能實現的,類似where方法一樣的處理: public DatabaseStore order(String key) { conditionStr += " order by " + key; return store; } 預設是升序。 API被人亂用的機率相當大,這時就需要有一些錯誤提示協助使用者定位問題了,最簡單的例子就是在沒有任何條件的情況下調用find方法,這時就應該提示沒有任何條件: if (conditionStr.equals("")) { throw new Throwable("There are not any conditions before find method invoked"); } 還有一種情況並不算是被亂用,但按照上面的實現是會出錯的: statuses = DatabaseStore.getInstance().order("updatetime").where("text", "我好").find(Status.class); 絕對會報錯,因為最後的SQL語句是這樣的:select * from status order by updatetime where text like '%我好%'。 這是不對的,必須將where放在order by前面。 解決這個問題的方法就是提供兩個字串: private String whereStr = "";private String orderStr = "";public DatabaseStore where(String key, String value) { whereStr += " where " + key + " like '%" + value + "%'"; return store;}public DatabaseStore order(String key) { orderStr += " order by " + key; return store;} 接著就是在find方法中進行判斷: if (whereStr.equals("") && orderStr.equals("")) { throw new Throwable("There are not any conditions before find method invoked");}String sql = "select * from " + clazz.getSimpleName().toLowerCase() + (whereStr.equals("") ? "" : whereStr) + (orderStr.equals("") ? "" : orderStr); 暫時就簡單實現了類似LitePal的ORM介面調用形式。