最近在看Android的ORM數據庫框架LitePal,就想到可以利用原生的SQLite來實現和LitePal類似的ORM接口實現。
LitePal有一個接口是這樣的:
List<Status> statuses = DataSupport.findAll(Status.class);
指定什么類型,就能獲取到該類型的數據集合。
這樣是很方便,於是想着自己不看它們的實現,自己搞一個出來。
首先想到的就是利用反射和泛型。
我想到了利用反射來調用set方法完成賦值。
Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { Log.e("DatabaseStore", field.getName()); }
Java的Class API有getFields和getDeclaredFields兩個方法,前者是用來獲取public字段的,后者是用來獲取所有聲明的字段的,顯然必須使用后者,而且注意的是,因為獲取到的字段是所有聲明的字段,所以絕對有可能獲取到不需要的字段。
cursor.getString(cursor.getColumnIndex("name"));
幸運的是,是可以獲取到的:
for (Field field : fields) { Type type = field.getGenericType(); Log.e("DatabaseStore", type.toString()); }
但如何知道哪些屬性是要被賦值的呢?
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關鍵字,這同樣也是種代碼約束。
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?
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 條件
其中,條件有四種匹配模式。
SELECT * FROM [user] WHERE u_name LIKE '%三%'
會把u_name中有“三”的記錄找出來。
SELECT * FROM [user] WHERE u_name LIKE '%三%' AND u_name LIKE '%貓%'
這樣能夠找出u_name中的“三腳貓”的記錄,但無法找到“張貓三”的記錄。
SELECT * FROM [user] WHERE u_name LIKE '_三_'
這樣只能找出“張三貓”這樣中間是“三”的記錄。
SELECT * FROM [user] WHERE u_name LIKE '三__';
這樣是找到“三腳貓”這樣“三”放在開頭的三個單詞的記錄。
SELECT * FROM [user] WHERE u_name LIKE '[張李王]三'
這樣是找到“張三”,“李三”或者“王三”的記錄。
SELECT * FROM [user] WHERE u_name LIKE '老[1-9]'
這將找出”老1“,”老2“。。。等記錄。
SELECT * FROM [user] WHERE u_name LIKE '[^張李王]三'
這樣找到的記錄就是排除”張三“,”李三“或者”王三“的其他記錄。
List<Status> myStatus = DataSupport.where("text=?", "我好").find(Status.class);
這樣的接口比較簡單,並且允許鏈式調用,形式上更加簡潔。
private String conditionStr; public DatabaseStore where(String key, String value) { conditionStr = " where " + key + " like '%" + value + "%'"; return store; }
為了實現鏈式調用,返回DatabaseStore是必須的。
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前面。
<?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
);
SELECT name FROM (SELECT * FROM sqlite_master UNION ALL SELECT * 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 '%我好%'。
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接口調用形式。