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

时间: 2024-10-12 19:47:31

Android SQLite的ORM接口实现(一)---findAll和find的实现的相关文章

Android 开源框架 ( 四 ) Afinal --- Android 里的 ORM IOC聚合型框架

Afinal 是一个android的sqlite的 orm 和 ioc 框架.是一种聚合型框架, 大而全.所以不推荐使用,只做了解即可.应付手头临时项目. 推荐阅读,这么多开源框架,该用哪个好?: 一.引言 Afinal是一个开源的android的orm和ioc应用开发框架.在android应用开发中,FinalActivity模块通过Afinal的ioc框架,诸如ui绑定,事件绑定,通过注解可以自动绑定.Afinal的orm框架,很轻松的就可以对android的sqlite数据库进行增删改查操

【Android基础】Android SQLite存储自定义对象

Android SQLite存储自定义对象 在SQLite数据库中可存储的数据类型有NULL.INTEGER.REAL(浮点型).TEXT.BOOL,一共是五种数据类型.在Android开发中,我们存储数据的一般的作法是数据库的属性就是类的成员变量,比如: 要存储一个人的姓名和年龄,在类中的是将它们定义为两个成员变量 class Person{ private String name; private int age; } 数据库中是将它们存储为两个字段 - name TEXT - age IN

Storm——Android SQLite数据库管理类库

Storm是一个Android SQLite数据库管理类库,可以通过注解创建表和迁移数据库.它不是ORM框架. 特性: 1.通过@Annotations创建表: 2.通过@Annotations迁移数据库: 3.接近于原生的SQLite insert.update.select操作的执行速度: 4.不需要手工解析游标/不需要手工初始化ContentValues: 5.Straight-forward API: 6.支持多个数据库文件: 7.Android-orientired library:

Android 开源框架 ( 五 ) xUtils --- Android 里的 ORM IOC聚合型框架

xUtils同Afinal一样属于聚合型框架, 大而全,但是越容易牵一发而动全身.所以不推荐使用,只做了解即可.应付手头临时项目. Android 开源框架 ( 四 ) Afinal --- Android 里的 ORM IOC聚合型框架 一.Xutils 介绍 Xutils是基于afinal开发的,但是比afinal稳定性提高了不少.xUtils 最初源于Afinal框架,进行了大量重构,使得xUtils支持大文件上传,更全面的http请求协议支持(10种谓词),拥有更加灵活的ORM,更多的事

Android sqlite cursor的遍历

查询并获得了cursor对象后,用while(corsor.moveToNext()){}遍历,当corsor.moveToNext()方法调用,如果发现没有对象,会返回false public List<MMImage> getAll() { List<MMImage> list = new ArrayList<MMImage>(); Cursor c = null; try { c = database.query(TABLE, null, null, null,

【原创】android——SQLite实现简单的注册登陆(已经美化)

1,Main_activity的xmL配置 1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 android:layout_width="match_parent" 4 android:layout_height="match_pa

Android Sqlite的操作

1.写一个类继承SQLiteOpenHelper public class MyHelper extends SQLiteOpenHelper { public MyHelper(Context context) { super(context, Const.DB_DBNAME , null, Const.DB_VERSION); // TODO Auto-generated constructor stub } @Override public void onCreate(SQLiteData

【原创】android——SQLite的cmd命令的基本操作

步骤:enter为按键 1,开始—>运行—>输入cmd  (enter) 2,输入adb shell  (enter) 3, cd data/data/应用包名/databases  (enter) 4, ls (查看目录下的数据库)   (enter) 5,sqlite3  数据库名.db;   (enter) 6,SQL语句操作表,注意标点符号一定是在英文输入法下 7,示例: [原创]android--SQLite的cmd命令的基本操作,布布扣,bubuko.com

Android中的Parcelable接口

Android中的android.os.Parcelable接口用于替代Java序列化Serializable接口,Fragment以及Activtity之间都需要传递数据,有时甚至包含结构非常复杂的对象,这就需要先将这个对象序列化成二进制流,然后再进行传递了. 比如Fragment1向Fragment2传递数据,下面是Fragment1中创建Fragment2并传送数据的方法: Fragment2 fragment = new Fragment2(); Bundle bundle = new