Implementation of the orm interface of Android SQLite (1) --- Implementation of findAll and find
Recently I was looking at the Android ORM database framework LitePal, and I thought we could use native SQLite TO IMPLEMENT THE ORM interface implementation similar to LitePal. LitePal has an interface like this: List <Status> statuses = DataSupport. findAll (Status. class); specifies the type to obtain the data set of this type. This is very convenient, so I thought that I did not look at their implementation and made it myself. The first thought is to use reflection and generics. There is a better way to use reflection: Annotation. when reading the annotation, you will know which attributes are to be assigned, but now I don't want to use annotation. What should I do? I thought of using reflection to call the set Method to complete the assignment. First, we need to know what fields need to be assigned values, and the reflection can get the fields, but unfortunately, it cannot determine the attribute name and type, and the Native SQLite operation needs to know the column name. Reflection indicates that the attribute name can be known: Field [] fields = clazz. getDeclaredFields (); for (Field field: fields) {Log. e ("DatabaseStore", field. getName ();} Java Class API has two methods: getFields and getDeclaredFields. The former is used to obtain public fields, and the latter is used to obtain all declared fields, obviously, the latter must be used, and note that, because the obtained field is all declared fields, it is absolutely possible to obtain unnecessary fields. However, it is not enough to know the attribute name. Android SQLite needs to know what type it wants to obtain: cursor. getString (cursor. getColumnIndex ("name"); fortunately, you can obtain: for (Field field: fields) {Type type = field. getGenericType (); Log. e ("DatabaseStore", type. toString ();} but how do you know which attributes are to be assigned? In terms of code constraints, we can require that all the attributes of the model be assigned values. It is unreasonable that the attributes of a model do not need to be assigned values, but in implementation, let's assume there is such a possibility. In this case, you need to get the setter. If there is a setter, it indicates that it needs to be assigned a value: List <Method> setMethods = new ArrayList <Method> (); for (Method method Method: allMethods) {String name = method. getName (); if (name. contains ("set ")&&! Name. equals ("offset") {setMethods. add (method); continue;} requires that the setter of all our attributes must carry the set keyword, which is also a code constraint. Since they are also code constraints, why can't attribute values be assigned directly? Unfortunately, this model may need to be serialized, but serialization may have a sequence ID, which does not need to be assigned a value, but it may exist in the model. Compared to this, as long as we use the setter automatically generated by the editor, there must be a set keyword, so this constraint is simpler. Then our operation is very simple: Determine whether the element in the Field name array has a corresponding setter. If so, retrieve the type of this attribute from the Field type array, then, determine the type, and retrieve the corresponding value from the table. Cursor cursor = Connector. getDatabase (). query (clazz. getSimpleName (), null, null); // query and obtain the cursor 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. getCol UmnIndex (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 (); In order to ensure universality, generic type is used, but here there is a small problem to be solved, that is, how to create a new T? This is not a joke, because T cannot be new, so it still needs to be done through reflection. It is necessary to obtain the constructor through reflection, but there may be many constructor. It is still a problem to obtain the best constructor. What is the best constructor? In fact, the Constructor of the model should basically be a non-argument Constructor, but just in case, we still need to pass a comparison: 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;} who has the least parameters and who is the best constructor, of course, 0 is the least. At this point, we basically implemented a database interface method with the same API as LitePal, but its internal implementation is the native method: List <Status> newData = DatabaseStore. getInstance (). findAll (Status. class); LitePal certainly provides the conditional query interface, that is, fuzzy query. The basic structure of fuzzy query is as follows: SELECT field FROM table WHERE a field Like condition, the condition has four matching modes. 1. %, indicating any 0 or more characters. It can match any type and length. In some cases, if it is Chinese, it must be %. SELECT * FROM [user] WHERE u_name LIKE '% 3%' will find records with "3" in u_name. You can use the and condition to add more conditions: SELECT * FROM [user] WHERE u_name LIKE '% 3%' AND u_name LIKE '% cat %' to find the "three-legged cat" record in u_name, however, you cannot find the "Zhang maosan" record. 2. _, indicating any single character, matching a single arbitrary character, used to limit the character length of the expression statement: SELECT * FROM [user] WHERE u_name LIKE '_ 3 _'. In this way, you can only find "three cats" in the middle. SELECT * FROM [user] WHERE u_name LIKE 'three _ '; this is to find the records of the three words starting with "three cats. 3. [], which indicates one of the characters listed in parentheses. Specify a character, string, or range. The matching object must be any of them. SELECT * FROM [user] WHERE u_name LIKE '[Zhang Li Wang] san'. In this way, find the records of "Zhang San", "Li San", or "Wang San. For example, if [] contains a series of characters (01234, abcde, and so on), it can be slightly written as "0-4", "a-e ": SELECT * FROM [user] WHERE u_name LIKE 'Old [1-9] '. This will find "Old 1", "old 2".. 4. [^] indicates a single character not listed in parentheses. The value is the same as [], but it requires that the matched object be any character other than the specified character. SELECT * FROM [user] WHERE u_name LIKE '[^ Zhang Li Wang] san' the records found here are excluded FROM other records of "Zhang San", "Li San", or "Wang San. 5. the query content contains wildcards. If we check special characters, such as "%" and "_", the general program should be included in "/", but "[]" is used in SQL. After learning the basic knowledge, we can start to see what the LitePal interface is like: List <Status> myStatus = DataSupport. where ("text =? "," Hello "). find (Status. class); this interface is relatively simple and allows chained calls, which is more concise in form. It is not difficult to achieve this. For now, we simply use a condition string to represent the conditions to be queried, and then provide a where method to concatenate where queries, currently, it is only a single condition: private String conditionStr; public DatabaseStore where (String key, String value) {conditionStr = "where" + key + "like '%" + value + "%'"; return store;} returned DatabaseStore is required for chained calling. The next step is very simple. You just need to splice the complete SQL statement and execute it: public <T> List <T> find (Class <T> clazz) {String SQL = "SELECT * FROM" + clazz. getSimpleName (). toLowerCase () + conditionStr; Cursor cursor = Connector ctor. getDatabase (). rawQuery (SQL, null); Field [] fields = clazz. getDeclaredFields (); List <String> fieldNames = new ArrayList <String> (); for (Field field: fields) {fieldNames. add (field. getName ();} List <Method> setMetho Ds = getSetMethods (clazz); List <T> list = getList (clazz, cursor, setMethods, fieldNames, fields); cursor. close (); conditionStr = ""; return list;} The getSetMethods method of the copied code is the encapsulation of the Code obtained from setter, the getList method is the encapsulation of the Code that generates the List of objects of the specified type. In this way, our interface method is called like this: List <Status> data = DatabaseStore. getInstance (). where ("text", "hello "). find (Status. class); whether it is LitePal or our own implementation, where must be placed before find. Here is a tip for getting the names of all tables in the database. At the underlying layer, we still use LitePal to create tables, while the LitePal is very simple. It is to put a litepal. xml file under the assets Folder: <? Xml version = "1.0" encoding = "UTF-8"?> <Litepal> <! -- Database name --> <dbname value = "xxx. db"> </dbname> <! -- Database version --> <version value = "1"> </version> <! -- Database table --> <list> <mapping class = "com. example. pc. model. status "> </mapping> </list> </litepal> but what is the table name? To confirm, we can query the names of all tables in the database: Cursor cursor = Connector. getDatabase (). rawQuery ("select name from sqlite_master where type = 'table' order by name", null); while (cursor. moveToNext () {// traverse the table name String name = cursor. getString (0); Log. e ("DatabaseStore", name);} Each SQLite database has a sqlite_master TABLE. The structure of this TABLE is as follows: create table sqlite_master (type TEXT, name TEXT, tbl_name TEXT, rootpage INTEGER, SQL TEXT); For tables, type The field is "table", the name field is the table name, and the index type is "index", the name is the index name, And tbl_name is the name of the table to which the index belongs. Whether it is a TABLE or an INDEX, the SQL fields are the command text when they were originally created using the create table or create index statement. For automatically created indexes, the SQL field is NULL. Sqlite_master indicates read-only, and its update can only be automatically updated through the create table, create index, drop table or drop index Command. Temporary tables will not appear in sqlite_master. Temporary tables, their indexes, and triggers are stored in another table named sqlite_temp_master. If you want to query the list of all tables including temporary tables, you need to write this statement: SELECT name FROM (SELECT * FROM sqlite_master union allselect * FROM sqlite_temp_master) WHERE type = 'table' order by name LitePal can also sort the results: list <Status> myStatus = DataSupport. where ("text =? "," Hello "). order ("updatetime "). find (Status. class); this is easy to implement, similar to the where method: public DatabaseStore order (String key) {conditionStr + = "order by" + key; return store ;} the default value is ascending. The probability of API misuse is quite high. In this case, some error prompts are required to help you locate the problem. The simplest example is to call the find method without any conditions, in this case, the system prompts that there are no conditions: if (conditionStr. equals ("") {throw new Throwable ("There are not any conditions before find method invoked");} another situation is not a mess, but according to the above implementation, there will be an error: statuses = DatabaseStore. getInstance (). order ("updatetime "). where ("text", "hello "). find (Status. class); it will definitely report an error because the final SQL statement is like this: select * from status order by updatetime where text like '% Hello % '. This is not correct. You must place where in front of order. To solve this problem, provide two strings: 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;} is then judged in the find method: if (whereStr. equals ("") & orderStr. equals ("") {throw new Throwable ("There are not any c Onditions before find method invoked ");} String SQL =" select * from "+ clazz. getSimpleName (). toLowerCase () + (whereStr. equals ("")? "": WhereStr) + (orderStr. equals ("")? "": OrderStr); at present, we have implemented a simple form of calling the ORM interface similar to LitePal.