greenDAO是一款优秀的对象关系映射(ORM)框架,能够提供一个接口通过操作对象的方式去操作关系型数据库,它能够让你操作数据库时更简单、更方便。和复杂麻烦的Android原生数据库API相比较,greenDAO可谓是简单实用,功能强大,不仅性能突出,而且有着丰富文档资料,是当前最为活跃的Android ORM框架。正因为greenDAO框架突出表现,其源码值得深入的研究。
本文的后续内容请见GreenDAO 3.2 源码分析(2):获得查询结果
查询(select)操作是数据库操作中最为重要的一个部分,本文将针对“GreenDAO中查询结果是如何生成”为线索,探究greenDAO的源码。
greenDAO框架中,针对每一个注解为@Entity的类,都会自动生成生成一个DAO(Database Access Object,数据访问接口)类,此类继承自抽象类AbstractDao<T, K>,包含方法queryBuilder(),来辅助生成Query类,以获得查询结果。
比如,对于一个Student类,greenDAO生成StudentDAO类,如果要获得数据库中所有student的信息,可以如下操作:
StudentDao.queryBuilder().list();
该函数将返回一个List<Student>,即数据库Student表中全部的数据。
下面我看看一下queryBuilder()的代码:
public QueryBuilder<T> queryBuilder() {
return QueryBuilder.internalCreate(this);
}
原来是生成一个QueryBuilder<T>对象,该对象是通过QueryBuilder.internalCreate(AbstractDao<T2, ?> dao)方法生成的,需要注意的是this指的是StudentDAO类,其继承自AbstractDao<T, K>。
QueryBuilder.internalCreate方法实际上是调用了QueryBuilder的构造器。
protected QueryBuilder(AbstractDao<T, ?> dao) {
this(dao, "T");
}
protected QueryBuilder(AbstractDao<T, ?> dao, String tablePrefix) {
this.dao = dao;
this.tablePrefix = tablePrefix;
values = new ArrayList<Object>();
joins = new ArrayList<Join<T, ?>>();
whereCollector = new WhereCollector<T>(dao, tablePrefix);
stringOrderCollation = " COLLATE NOCASE";
}
让继续看看QueryBuilder对象的list()方法:
public List<T> list() {
return build().list();
}
这个方法其实分为两部分:
- 首先调用build方构建SQL以及Query实例;
- 然后调用list方法得到查询救过
下面我们就分为这两个部分来讲解,本文的重点在第一部分,第二部分将在后续的文章中为大家介绍。
1. 如何生成select语句
/**
* Builds a reusable query object
* Query objects can be executed more efficiently
* than creating a QueryBuilder for each execution.
*/
public Query<T> build() {
//生成查询语句
StringBuilder builder = createSelectBuilder();
//获得返回结果的限制数,即最多返回几行数据
int limitPosition = checkAddLimit(builder);
//获得开始查询的位移行数,即从第几行开始查询
int offsetPosition = checkAddOffset(builder);
String sql = builder.toString();
//检查是否输出所生成的sql语句,以及对应的参数
checkLog(sql);
//创建Query对象,通过该对象来正真地查询数据
return Query.create(dao, sql, values.toArray(), limitPosition, offsetPosition);
}
bulid()方法中做了好多事情,简而言之就是在生成了查询语句,并以此返回Query对象:
- 生成包含select语句的StringBuilder;
- 为select语句添加LIMIT部分和OFFSET部分;
- 以String形式产生完整的select sql语句,并根据设置在log中输出sql语句;
- 以线程为单位生成Query类实例。
咱们一一来看。
1.1 createSelectBuilder()
首先createSelectBuilder()方法用来生成select语句,并将生成的select语句保存在StringBuilder中:
private StringBuilder createSelectBuilder() {
//构造select语句主题部分,包括从那个表中查询哪些字段
String select = SqlUtils.createSqlSelect(dao.getTablename(), tablePrefix, dao.getAllColumns(), distinct);
StringBuilder builder = new StringBuilder(select);
//向select语句中添加联合查询以及条件查询部分
appendJoinsAndWheres(builder, tablePrefix);
//向select语句中添加order部分
if (orderBuilder != null && orderBuilder.length() > 0) {
builder.append(" ORDER BY ").append(orderBuilder);
}
return builder;
}
如果读者对于SQL语句很熟悉,应该已经想到* SqlUtils.createSqlSelect*方法在做些什么了,就是根据要查询的字段生成sql语句,以下是源码:
/** Creates an select for given columns with a trailing space */
public static String createSqlSelect(String tablename, String tableAlias, String[] columns, boolean distinct) {
//判断表的别名是否存在,如果表别名无效,则抛出异常
if (tableAlias == null || tableAlias.length() < 0) {
throw new DaoException("Table alias required");
}
// 根据distinct的值,来判断是否要在select语句中加入“ DISTINCT”关键字
StringBuilder builder = new StringBuilder(distinct ? "SELECT DISTINCT " : "SELECT ");
// 添加要查询的列,以及关键字“FROM”
SqlUtils.appendColumns(builder, tableAlias, columns).append(" FROM ");
// 添加表名和表的别名
builder.append('"').append(tablename).append('"').append(' ').append(tableAlias).append(' ');
return builder.toString();
}
createSqlSelect方法返回的sql语句是select语句,相当于是产生“select * from tableName ”,其本质就是通过StringBuilder构造String对象。这里需要解释的是为什么表的别名* tableAlias*是必须的,这是为了在处理多表联合查询时方便处理,多表的联合查询中往往需要给表取别名以方便构建SQL,因此在此处要求必须有别名,所以对于单表查询没有意义,但是在联合查询中却很有帮助,这里也体现了greenDAO设计团队的良苦用心。
下面我们来看下* appendJoinsAndWheres*方法,顾名思义这个方法是为selelct语句添加联合查询和where语句部分:
private void appendJoinsAndWheres(StringBuilder builder, String tablePrefixOrNull) {
//清空sql语句参数
values.clear();
//添加表连接部分
for (Join<T, ?> join : joins) {
builder.append(" JOIN ").append(join.daoDestination.getTablename()).append(' ');
builder.append(join.tablePrefix).append(" ON ");
SqlUtils.appendProperty(builder, join.sourceTablePrefix, join.joinPropertySource).append('=');
SqlUtils.appendProperty(builder, join.tablePrefix, join.joinPropertyDestination);
}
//根据whereAppended的值,添加where条件
boolean whereAppended = !whereCollector.isEmpty();
if (whereAppended) {
builder.append(" WHERE ");
whereCollector.appendWhereClause(builder, tablePrefixOrNull, values);
}
// 添加连接条件
for (Join<T, ?> join : joins) {
if (!join.whereCollector.isEmpty()) {
if (!whereAppended) {
builder.append(" WHERE ");
whereAppended = true;
} else {
builder.append(" AND ");
}
join.whereCollector.appendWhereClause(builder, join.tablePrefix, values);
}
}
}
这里都是纯粹的SQL语句的生成,相当于是一种编译器,各种条件参数转化为标注的SQL语句,不难理解,就不在赘言了。如果理解有困难,建议去参看关于SQL查询的文章。
1.2 LIMIT & OFFSET
首先要说明下LIMIT & OFFSET的意义,这部分在SQL语句中不是那么常见。假设数据库表student存在13条数据。
语句1:select * from student limit 9,4
语句2:slect * from student limit 4 offset 9
语句1和2均返回表student的第10、11、12、13行,语句2中的4表示返回4行,9表示从表的第十行开始。也就是说 LIMIT表示查询结果返回的行数的限制,OFFSET表示开始从第几行开始查询。注意OFFSET关键字必须和LIMIT联合使用,不能单独使用。
checkAddLimit和* checkAddOffset*方法的代码相似,就放在一起分析:
private int checkAddLimit(StringBuilder builder) {
int limitPosition = -1; //limitPosition表示 LIMIT的值在List<Object> values中的位置
if (limit != null) { //如果QueryBuilder中设置了limit的值
//为select语句添加 LIMIT部分
builder.append(" LIMIT ?");
// 将limit的值保存在values中
values.add(limit);
//记录下limit值values中的位置
limitPosition = values.size() - 1
}
return limitPosition;
}
private int checkAddOffset(StringBuilder builder) {
//offsetPosition表示OFFSET的值在List<Object> values中的位置
int offsetPosition = -1;
if (offset != null) {
if (limit == null) { //如果没有这事limit, 是不能设置offset
throw new IllegalStateException("Offset cannot be set without limit");
}
//为select语句添加 OFFSET部分
builder.append(" OFFSET ?");
//将offset的值保存在values中
values.add(offset);
//记录下offset值values中的位置
offsetPosition = values.size() - 1;
}
return offsetPosition;
}
代码很直观并不复杂,需要注意的有两点:
- 为什么前面提到的中的createSelectBuilder方法要返回StringBuilder?这是因为后面可能还要继续构建select语句添加LIMIT&OFFSET部分,如果返回String类型再对其修改,效率较低;
- 为什么要把limit和offset的值都写入到valuses中,而不是直接写入sql语句?因为在Query实例向数据库查询数据时还需要用到limit和offset的值,那时再从sql语句中提取出反而麻烦。
1.3 显示SQL语句和查询参数
因为greenDAO是自动生成数据库和查询语句,用户是不直接操控数据库的,所以一旦遇到什么问题不是很容易定位。如果在调试的过程中想要观察产生的sql语句是否和预期一致,可以是设置参数* QueryBuilder.LOG_SQL和 QueryBuilder.LOG_VALUES*
QueryBuilder.LOG_SQL = true;
QueryBuilder.LOG_VALUES = true;
checkLog方法会根据这两个值来判断是否要在日志中输出所生成的SQL和查询参数
private void checkLog(String sql) {
if (LOG_SQL) {
DaoLog.d("Built SQL for query: " + sql);
}
if (LOG_VALUES) {
DaoLog.d("Values for query: " + values);
}
}
1.4 生成Query类对象
有了要查询的SQL语句,下面就是要执行SQL操作并得到结果集。Query类就是负责具体执行SQL语句的实体对象。下面我们来重点分析Query类的源码。
static <T2> Query<T2> create(AbstractDao<T2, ?> dao, String sql, Object[] initialValues, int limitPosition, int offsetPosition) {
//QueryData类,是Query类的静态内部类,用来保存查询的数据
QueryData<T2> queryData = new QueryData<T2>(dao, sql, toStringArray(initialValues), limitPosition,
offsetPosition);
// 获得为当前线程分配的Query对象,也就是为每个线程分配单独的Query实例
return queryData.forCurrentThread();
}
Query.create方法是工厂模式,以Query的静态方法返回Query类对象。值得注意的是,create方法没有直接去创建Query对象,而是先创建QueryData对象,再通过QueryData对象创建Query对象,如此“大费周章”其实大有深意,原因在于为了实现Query对象的复用与进程独立,这也是greenDAO设计的精巧之处,让我们快来一探究竟吧。
private final static class QueryData<T2> extends AbstractQueryData<T2, Query<T2>> {
private final int limitPosition;
private final int offsetPosition;
QueryData(AbstractDao<T2, ?> dao, String sql, String[] initialValues, int limitPosition, int offsetPosition) {
super(dao, sql, initialValues);
this.limitPosition = limitPosition;
this.offsetPosition = offsetPosition;
}
@Override
protected Query<T2> createQuery() {
//调用Query的构造函数时,将自身也传入其中,将QueryData和Query联系在一起
return new Query<T2>(this, dao, sql, initialValues.clone(), limitPosition, offsetPosition);
}
}
QueryData类继承自抽象类AbstractQueryData,其中Q createQuery()方法为抽象方法,要求QueryData必须实现,从上面的代码中可以看出,QueryData在实现createQuery()中调用了Query的构造函数,其参数中也包括QueryData对象本身(this),Query对象将会保存这个QueryData对象到自己的queryData域中,这样就将二者绑定了起来。
createQuery方法将会在forCurrentThread中被调用,该方法用来获得为当前线程所分配的Query对象。
在greenDAO的多线程查询机制中,会为每一个查询线程都分配一个单独的Query对象,在一个线程中如果使用和该线程不匹配的Query对象去查询,将会报错。这种设计机制避免引入上锁解锁,不光提高了效率,还让代码会更为简洁。
接下来就到了本文的重点* forCurrentThread*方法的代码分析:
/**
* Note: all parameters are reset to their initial values specified in {@link QueryBuilder}.
*/
Q forCurrentThread() {
// Process.myTid() seems to have issues on some devices (see Github #376) and Robolectric (#171):
// We use currentThread().getId() instead (unfortunately return a long, can not use SparseArray).
// PS.: thread ID may be reused, which should be fine because old thread will be gone anyway.
// 获得当前进程号
long threadId = Thread.currentThread().getId();
//queriesForThreads是保存进程ID和Query对象之间对应关系的Map
synchronized (queriesForThreads) {
//尝试获取当前进程号所对应的Query对象
WeakReference<Q> queryRef = queriesForThreads.get(threadId);
//如果进程号有对应的Query对象,则将其赋值给query,否者赋为空引用
Q query = queryRef != null ? queryRef.get() : null;
//如果query为空引用
if (query == null) {
//垃圾回收
gc();
//创建新的Query对象
query = createQuery();
//将线程号和query对象保存到queriesForThreads中
queriesForThreads.put(threadId, new WeakReference<Q>(query));
} else {//如果query不为空
//initialValues中的查询参数直接拷贝到query.parameters
System.arraycopy(initialValues, 0, query.parameters, 0, initialValues.length);
}
return query;
}
}
forCurrentThread工作流程如下:
- 依据当前ThreadID, 尝试从Map queriesForThreads取出对应的Query对象;
- 如果能获得Query对象,则将查询参数拷贝给该Query对象
- 如果不能获得Query对象,则说明目前还没有为该线程分配Query对象,需要新建Query实例,并将该实例和线程号保存到queriesForThreads中;
- 返回Query对象
正如上文所说,QueryData中维护着线程号和Query对象的对应表
final Map<Long, WeakReference<Q>> queriesForThreads;
新线程的将会分配新的Query对象,旧的线程将会取回原来为它分配Query对象,从而保证各个进程所操作的Query对象是独立的,避免多线程的冲突。在给新线程分配新的Query对象时,采用了对queriesForThreads的同步操作,避免多线程冲突。
这里要特别说明下queriesForThreads的类型是**Map<Long, WeakReference<Q>> **,Map中的value对应的类型是Query对象的弱引用,这样是为了防止内存泄露。如果不用弱引用而是直接引用,会发生如下问题:
- QueryData实例QD中的Map对象queriesForThreads包含着一系列的Query对象;
- Query类中存在一个域保存QueryData变量,故而queriesForThreads中的Query对象的queryData域都会是QD的引用;
- Query对象和QueryData对象将会相互引用;
- Java GC机制将永远不会回收Query对象,即使它已经执行完毕,不再使用。
WeakReference的使用就是保证queriesForThreads中对Query的引用不会影响垃圾回收,破除相互引用带来的内存泄露。当Query对象被回收之后,QueryData类可以通过gc方法回收queriesForThreads中无用的键值对:
void gc() {
synchronized (queriesForThreads) {
//获得迭代器
Iterator<Entry<Long, WeakReference<Q>>> iterator = queriesForThreads.entrySet().iterator();
while (iterator.hasNext()) {
Entry<Long, WeakReference<Q>> entry = iterator.next();
//如果发现是query对象为null
if (entry.getValue().get() == null) {
iterator.remove();//删除该键值对
}
}
}
}
总结
上文已经介绍太多内容了,是时候进行下总结:
- QueryBuilder用建造者模式(Builder Pattern)帮助构建查询所用的SQL语句,并以此来生成Query对象;
- 但是QueryBuilder并不是直接生成Query实例,而是通过Query类的内部静态类QueryData生成Query对象,内部静态类的优势在于不用依靠外部类实例就能单独使用,同时又可以使用外部类的静态生产成员;
- 为了解决多线程查询的问题,greenDAO并不是为查询表上锁,而是通过QueryData对象为每个线程都创建独立的Query对象;
- 线程和Query对象的对应关系,被以键值对的形式保存在QueryData对象内部,同一个线程中,Query对象将被复用;
- 为了避免由于相互引用而带来的内存泄漏,Map<Long, WeakReference<Q>> queriesForThreads中以弱引用的形式引用Query对象。
当得到Query对象之后,下一步就应该是向数据库查询数据,得到游标返回结果集,但是由于篇幅的关系,这部分内容将在下一篇文章GreenDAO 3.2 源码分析(2):获得查询结果中继续和大家分享,敬请期待。
欢迎大家留言讨论与指正。