最近在看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 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 '%我好%'。
這是不對的,必須將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接口調用形式。