225  
查询码:00000732
Android流行ORM框架性能对比及Room踩坑总结
来源:https://sq.163yun.com/blog/article/177878979956150272
作者: 朱凡 于 2021年02月10日 发布在分类 / FM组 / FM_App 下,并于 2021年02月10日 编辑
数据 方法 语句 我们 操作 使用 调用 完成 insert 数据库

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

把生命浪费在美好事物上2018-07-18 11:37

修改

在工作中发现数据库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();
}


 推荐知识

 历史版本

修改日期 修改人 备注
2021-02-10 22:21:05[当前版本] 朱凡 创建版本

 附件

附件类型

PNGPNG

知识分享平台 -V 4.8.7 -wcp