Android流行ORM框架性能对比及Room踩坑总结

在工作中发现数据库ORM耗时较多,影响了用户体验,恰巧谷歌在2017IO大会上推出了新的ORM框架Room,该框架和其他Android流行ORM框架有什么不同?该框架的ORM过程是怎样的?其他框架的ORM过程又为什么比他慢?迁移到Room又有什么坑需要注意?本篇文章解答这些问题。
本篇文章分为三部分,首先介绍Android流行ORM框架ORMLite、GreenDao、Room的一些特性。随后介绍他们的ORM过程。最后介绍从ORMLite迁移到Room我们需要注意的坑。
一、ORMLite介绍
ORMLite是一款通过反射完成对象关系映射的数据库 框架。其Android部分在github上有1.2k的star和0.3k的Fork。其使用简单,但由于使用反射,造成了一定的性能开销,其自身提供了ormlite_config机制通过读取文件内容绕过反射来创建数据表。其主要特性如下:
1.使用反射来完成对象关系的映射,速度较慢。
2.使用类sql描述sql语句,比如where.eq("name",name).and().eq("deleteTime",0);。
3.在insert操作后自动设置数据的主键。
4.支持将父类的变量解析为数据库表字段。
5.支持sqlcipher。
6.提供connectionProxy,用于在CRUD等操作时进行统一的逻辑操作,如发送事件等。 
二、GreenDao介绍
GreenDao是Android平台的一款流行的对象关系映射数据库 框架,在github上有8.4k的star和2.4k的fork,jar包大小140KB。
其主要特性如下:
1.使用自定义的gradle插件来完成sql相关代码的生成。该插件在GreenDao3.0版本后才开始支持,在3.0之前需要我们引入一个greendao generated项目用来生成代码。
2.它使用类sql来表示sql语句,类似ORMLite。
3.他支持懒加载,在查找时,首先返回一个cursor,在我们需要使用到具体数据时,才将之前得到cursor转变为实体对象。
4.支持sqlcipher。
5.不支持将父类的变量解析为数据库表字段。 
三、Room介绍
Room同样为Android平台的一款对象关系映射 框架,其为2017年谷歌IO推出的Android Architecture Component的一部分,其主要特性如下:
1.其使用谷歌官方的注解处理器annotationProcessor完成对注解的解析。
2.使用原生sql来表达对数据库的操作,会在编译时会验证字段名称是否匹配,如果有问题,则发生编译错误,而不是运行时故障。
3.它还支持同为Android Architecture Component的LiveData,实现数据的动态刷新和绑定组件生命周期功能。
4.他并不支持sqlcipher,需要我们使用第三方库来支持。
5.支持父类变量解析为数据库表字段。
6.默认会让主线程的数据库查询操作崩溃,可以通过allowMainThreadQueries绕过这个限制。 
四、一次insert操作引起的ORM及性能对比
这里以插入操作举例,看看Ormlite、GreenDao、Room都是怎样完成ORM的。首先我们需要清楚sql操作的具体执行过程,其包含两大部分,编译和执行,编译阶段又可以分为四个部分:首先是sql解析,也就是检查sql语法错误,检查sql中涉及的表和字段是否存在,生成语法树等,然后是编译,优化,最后是存储到缓存中,便于避免再次解析编译等的开销,在完成sql编译后,会传入我们具体的参数,比如插入的值,查询的值等,就是如下图的placeHolder replacement,最后就是执行。
那么一次标准的insert操作的执行过程就为:先构造sql语句,insert into Broker (serverId,name) values(?,?); 随后我们使用SqliteDatabase的compileStatement来编译该语句,编译完语句后,我们使用bindLong、bindString等传入参数,最后执行executeInsert完成数据库的一次插入操作。
对于ORMLite,如下图所示,一次insert操作,包括首先调用MappedCreate的build方法,完成sql语句的构造,随后根据我们传入的实体对象,调用FieldType的extractRawJavaFieldValue反射调用对象的get方法获取属性值数组,随后将得到的sql语句和属性值数组传给DatabaseConnection类的insert方法,在该方法中完成sql语句的编译、属性值的绑定和sql语句的执行。所以可以看到根据对象得到属性值的过程是通过反射的,速度慢。
再来看下GreenDao的插入操作,如下图所示,GreenDao的一次插入操作很标准,首先我们调用Dao的insert操作后,该方法会调用TableStatements的getInsertStatement,其中又会调用到SqlUtils的createSqlInsert操作完成sql语句的拼接,随后在TableStatements的getInsertStatement中又调用database的compileStatement完成sql语句的编译,生成DatabaseStatement,随后调用我们在make project时生成的BrokerEntityDao的bindValues方法完成属性值的绑定,最后直接executeInsert即可。
最后是Room的一次insert操作,如下图所示,首先我们调用的是IBrokerDao_IMPL的insert方法,该方法会调用SharedSQLiteStatement的acquire方法,其中会调用到我们生成的IBroker_IMPL的createQuery方法获取sql语句,这里就不同于GreenDao了,GreenDao中会在代码中完成sql的拼接,而Room则会根据我们在接口方法上的注解生成具体的sql语句,随后编译该sql语句,编译完成后调用生成的IBroker_IMPL的bind方法,完成参数的绑定,随后调用executeInsert完成数据的插入。
我们用图表来对比一下ORMLite、GreenDao和Room。对于insert操作,ORMLite由于在得到bind参数时使用反射,速度最慢,GreenDao使用事先生成的代码进行bind,但是其生成sql语句是通过字符串拼接,会有一点时间损耗,而Room则更彻底,连sql语句都为我们生成好。其性能最好。
update、get也是类似的,这里ORMLite由于没有updateList的方法,这里的时间还加上了list循环的开销。GreenDao都会调用SqlUtils的createSqlSelect和createSqlUpdate语句生成sql。只不过GreenDao的daoSession有缓存机制,直接从内存中查找。所以GreenDao的get有时候也会快于Room。
五、迁移到Room踩坑总结
1.Room在版本alpha8中给原始数据类型在初始化时加上了not null限制


而由于ORMLite默认允许null,便使得迁移前后的数据库字段的限制不一样,这样在从ORMLite迁移到Room的upgrade中的validateMigration方法便会抛出异常。如何解决? 首先考虑创建新的数据库表,加上not null限制,并将之前的数据加上默认值拷贝过来,但是更改表名后,发现低版本升级语句也需要重新修改表名,工作量比较大,因此考虑创建一个临时表,具体过程为:先执行低版本数据表升级语句,随后创建临时表Table_Temp,再将原来表数据迁移到临时表Table_Temp,其中迁移语句需要加上ifnull("intColumn",0),防止由于not null限制导致报错,删除原来表Table,再创建新表Table,包含not null限制,将临时表Table_Temp数据迁移到Table,删除临时表Table_Temp,经测试,上述过程在2000条数据情况下需要花费500ms左右。
另外由于Room不支持直接在java类中声明默认值,对于那些包含原始数据类型的初始化语句也需要我们加上默认值。
2.Room并不支持sqlcipher,需要我们使用 第三方库来支持。 
SafeHelperFactory factory = new SafeHelperFactory(getDBKey().toCharArray());
AppDatabase mDb = Room.databaseBuilder(
                         MoneyKeeperApplication.getAppContext(), AppDatabase.class, DATABASE_NAME)
                        .allowMainThreadQueries().openHelperFactory(factory).build();
3.我们可以在创建AppDatabase时传入的callback的onCreate、onOpen方法中完成数据库表初始数据的插入,此时不能使用Room相关的CRUD方法,因为这些方法会调用getWritableDatabase方法,而sqlcipher中的getWritableDatabase方法中会通过变量mIsInitializing检查上次调用是否结束,如果没有结束便会抛出异常getWritableDatabase called recursively。我们可以直接使用onCreate或者onOpen方法参数SupportSQLiteDatabase执行execSql方法,传入原生的sql语句来解决该问题。
4.Room并没有像ORMLite一样提供connectionProxy,这样如果我们需要在数据库CRUD操作时插入特殊操作如发送数据表变化事件,就不能使用代理类统一处理,我的解决方法是将特殊操作直接添加在BL层的抽象父类的方法中:
public abstract class AbsDBBL {

     public abstract IBasicDao getDao();

     public long insert(EntityType data) {
          long ret = getDao().insert(data);
          postChangeEvent(EntityType.TABLE_NAME);
          return ret;
     }
}
如上代码所示,其中getDao得到Room通过注解生成的包含具体sql语句的接口实现类,在我们调用insert方法后再调用发送事件方法。
5.Room在insert语句后并不会像ORMLite一样设置插入数据的主键,这样就需要我们手动完成该工作。代码如下:
public long insert(EntityType data) {
          long now = System.currentTimeMillis();
          data.setClientCreateTime(now);
          data.setClientModifyTime(now);
          long id = super.insert(data);
          data.setId(id);
          return data.getId();
}

本文来自网易实践者社区,经作者冯越新授权发布。