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 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接口調用形式。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM