android sqlite insertorthrow,GreenDao与Room对比以及Android SQLite API优化

66b52468c121889b900d4956032f1009.png

8种机械键盘轴体对比

本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选?

主流库

目前Android主流的ORM相关库可以分为两类,一类是我们熟知的基于SQLite并进行一系列封装和优化的框架,比如GreenDao、Room、DBFlow等;另一类是NoSql数据库(注意这一类是数据库,是SQLite的替代品),比如Realm。

NoSql

NoSql database也可以称为no relational database,即非关系型数据库。与关系型数据库不同,NoSql不存在复杂的关系模型,在数据库设计上更自然,没有了复杂的关系型模型,维护起来也更加容易。比如一篇博文中包含正文、评论等,关系型数据库在设计时会根据三范式,将正文和评论放到不同的表中,使用时需要进行联合查询。而面向文档的NoSql数据库可以将正文和评论都保存在一个文档中。在查询时,NoSql直接读取这一个文档即可,相比关系型数据库的多次查找会更快。而且NoSql更方便扩展,在分布式集群上,普通的关系型数据库的拆分与查询举步维艰(而且关系型数据库有数据量限制),而NoSql可以轻易地将不同的文档或者键值对保存在不同的服务器上,只需要使用合适的算法,查询也不是难事。

但是NoSql的缺点也同样明显。首先是难以实现复杂的查询。在关系型数据库中,我们往往可以轻易实现多表复杂联查,但是NoSql不支持联合查询,需要上层代码进行更多的处理,比如使用多次查询。其次是占用存储空间更多。运用关系型数据库三范式能够设计出精简巧妙的数据库,很好地降低数据冗余。但是NoSql则不然。比如上面提到的博文的例子,关系型数据库的“评论”表中只需要记录评论者的ID、评论文章ID、评论内容即可,至于评论者的详细信息,则由User表提供。但是在NoSql中,为了避免使用多次查询(因为多次查询需要上层代码进行更多处理),我们可能需要把博文中需要显示出来的评论者的信息——用户名、用户头像等——都保存在同一个文档中。这在一定程度上增加了存储空间的占用。试想,如果一个活跃用户评论了绝大多数的文章,那他的用户名和头像信息冗余就会非常严重。

在移动平台上,基于SQLite的ORM框架还有一个优势,就是引入之后不会对应用安装包体积产生明显的影响。基于以上对比,可以认为基于SQLite的ORM框架更加适合移动平台。

SQLite Based ORMs的选择

一般选择以下两个:

直至目前,GreenDao都无疑是Android SQLite Based ORMs中的带头大哥,无论是Github Star数,还是各类博客中出现的次数,都稳居第一。而谷歌推出的持久化框架Room则有望与之齐平。

本文就选择这两个框架与传统SQLite API进行对比。

对比

分别从易用性和速度上进行对比。

易用性对比GreenDaoRoom开发者只需要规定Entity的属性即可需要规定Entity的属性,需要规定Dao接口

每次更新Entity需要重新build以生成代码,大型项目build耗时会比较久更新Entity不需要重新build,因为使用的是Dao接口规定的方法,但是需要根据情况更新Dao接口

只有进行复杂操作时才需要写SQL语句即使是进行简单的条件查询,也要写SQL语句

有一定的学习成本,需要学习注解、查询、条件语句等API学习成本低,基本只需要掌握Room的注解即可

很明显,如果熟悉SQL语句,那么Room会更容易上手,在开发中也相对更加便利。

速度对比

速度对比项有:插入数据 - 插入1000条user数据和10000条message数据

查询全部 - 查询所有(10000条)message数据

使用每一个框架创建Disk Database,针对以上对比项分别测试50次并计时,结果取平均值,时间为ms。具体代码可以参考android-orm-benchmark来实现,这里就不贴代码了。在HUAWEI Mate 10 Android 8.0.0上运行,得出结果如下:传统SQLite APIGreenDaoRoom插入数据565327711441

查询全部611750411

可以看出,Room在查询和写入上的速度超过了传统SQLite API和GreenDao,甚至GreenDao在查询速度上会比传统SQLite API慢。

分析

写入

为什么Room和GreenDao在写入的速度上会比传统SQLite API快这么多?首先来看一下每次传统SQLite API在写入时执行的几个步骤:组装ContentValues

拼接SQL语句

将ContentValues转换为数组

创建SQLiteStatement

将SQLiteStatement与第3步的数组绑定

执行写入操作

而Room只需要两步:将SQLiteStatement与要插入的数据绑定

执行写入操作

这就是Room比传统SQLite API快的原因。通过缓存SQLiteStatement,避免了每写一行都需要重新拼接SQL语句、创建SQLiteStatement这些重复操作。同时,由于掌握了SQLiteStatement,可以直接绑定数据,不再需要依赖ContentValues,避免了组装和反组装的耗时。

而由于GreenDao会把要写入的对象缓存起来,如果已经缓存过就更新,以此来提高查询速度,所以会有更多的时间消耗。实际上,在速度对比这一节中,我们先清除了GreenDao的对象缓存再执行查询,因为缓存无法命中,需要使用Cursor生成对象。尽管GreenDao在遍历Cursor生成对象这一步有所优化,但是其对速度的提升幅度相对查询缓存来说是很小的,所以其速度会比传统SQLite API更慢。在上述测试代码中,如果不清除缓存,GreenDao查询速度会有2~3倍的提升(随着数据量的增加,这个提升会有所下降)。由于GreenDao的缓存对象与返回的查询结果是同一对象,因此需要特别注意,在开启缓存的情况下,如果直接修改查询出来的对象,而且没有及时同步到数据库中,那么就会导致下次查询结果错误与数据库不同步。1

2

3

4

5

6

7

8

9DaoSession session = mDaoMaster.newSession();

Message msg = session.getMessageDao().queryBuilder()

.where(MessageDao.Properties.Id.eq(1)).unique();;

Log.e("before", msg.toString());

msg.content = "modified";

Message message1 = session.getMessageDao().queryBuilder()

.where(MessageDao.Properties.Id.eq(msg.id)).unique();

Log.e("after", message1.toString());

输出如下:1

2E/before: 1, JkJsHimdMNwLF9WWsJR8Vj5uea5uqPDlSzve2raGK6t0u6qwEZopLbi80KHDapqcEJiugixqHqUhhavmlOz9OGB2EnFAD3Mj9Qnt

E/after: 1, modified

考虑到部分项目可能会选择关闭这个功能(如果你的项目中确定要关闭GreenDao缓存,那么Room会是你更好的选择),因此测试代码选择每次执行数据库操作后,清除缓存。

查询

同样地,传统SQLite API执行查询也需要每次都拼接SQL语句。而且在查询完成得到Cursor之后,我们在使用传统SQLite API生成对象时,一般会在循环体中进行列下标查询,即:1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20while (c != null && c.moveToNext()) {

Message newMessage = new Message();

newMessage.setChannelId(c.getLong(c

.getColumnIndex(Message.CHANNEL_ID)));

newMessage.setClientId(c.getLong(c

.getColumnIndex(Message.CLIENT_ID)));

newMessage.setCommandId(c.getLong(c

.getColumnIndex(Message.COMMAND_ID)));

newMessage.setContent(c.getString(c

.getColumnIndex(Message.CONTENT)));

newMessage.setCreatedAt(c.getInt(c

.getColumnIndex(Message.CREATED_AT)));

newMessage.setSenderId(c.getLong(c

.getColumnIndex(Message.SENDER_ID)));

newMessage.setSortedBy(c.getDouble(c

.getColumnIndex(Message.SORTED_BY)));

messages.add(newMessage);

}

而Room只会查询一次下标:1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26// Code generated by Room

final int _cursorIndexOfId = _cursor.getColumnIndexOrThrow("_id");

final int _cursorIndexOfClientId = _cursor.getColumnIndexOrThrow("client_id");

final int _cursorIndexOfCommandId = _cursor.getColumnIndexOrThrow("command_id");

final int _cursorIndexOfSortedBy = _cursor.getColumnIndexOrThrow("sorted_by");

final int _cursorIndexOfCreatedAt = _cursor.getColumnIndexOrThrow("created_at");

final int _cursorIndexOfContent = _cursor.getColumnIndexOrThrow("content");

final int _cursorIndexOfSenderId = _cursor.getColumnIndexOrThrow("sender_id");

final int _cursorIndexOfChannelId = _cursor.getColumnIndexOrThrow("channel_id");

final RoomMessage[] _result = new RoomMessage[_cursor.getCount()];

int _index = 0;

while(_cursor.moveToNext()) {

final RoomMessage _item;

_item = new RoomMessage();

_item.id = _cursor.getLong(_cursorIndexOfId);

_item.clientId = _cursor.getLong(_cursorIndexOfClientId);

_item.commandId = _cursor.getLong(_cursorIndexOfCommandId);

_item.sortedBy = _cursor.getDouble(_cursorIndexOfSortedBy);

_item.createdAt = _cursor.getInt(_cursorIndexOfCreatedAt);

_item.content = _cursor.getString(_cursorIndexOfContent);

_item.senderId = _cursor.getLong(_cursorIndexOfSenderId);

_item.channelId = _cursor.getLong(_cursorIndexOfChannelId);

_result[_index] = _item;

_index ++;

}

return _result;

SQLite API优化

其实通过上一节的分析,大家应该已经有了优化的思路。没错,就是照抄Room:自己生成和管理SQLiteStatement,杜绝ContentValues和拼接SQL语句1

2

3

4

5

6

7

8

9

10SQLiteStatement insertMessage = db.compileStatement(

String.format("Insert into %s (%s, %s, %s, %s, %s, %s, %s) values (?,?,?,?,?,?,?)",

OptimizedMessage.TABLE_NAME,

OptimizedMessage.CONTENT,

OptimizedMessage.SORTED_BY,

OptimizedMessage.CLIENT_ID,

OptimizedMessage.SENDER_ID,

OptimizedMessage.CHANNEL_ID,

OptimizedMessage.COMMAND_ID,

OptimizedMessage.CREATED_AT ));

在遍历Cursor前获取列下标,而不是在循环体中1

2

3

4

5

6

7

8// Do these things outside the while syntax

int channelIdIndex = c.getColumnIndex(OptimizedMessage.CHANNEL_ID);

int clientIdIndex = c.getColumnIndex(OptimizedMessage.CLIENT_ID);

int commandIdIndex = c.getColumnIndex(OptimizedMessage.COMMAND_ID);

int contentIndex = c.getColumnIndex(OptimizedMessage.CONTENT);

int createdAtIndex = c.getColumnIndex(OptimizedMessage.CREATED_AT);

int senderIdIndex = c.getColumnIndex(OptimizedMessage.SENDER_ID);

int sortedByIndex = c.getColumnIndex(OptimizedMessage.SORTED_BY);

以上两点就是通过对比Room得出的优化方案,应用到测试代码中之后,写入和查询速度有了明显的提升:优化前优化后GreenDaoRoom写入5653186527711441

查询611536750411

除了上述借鉴Room和GreenDao的优化之外,还有一些基本知识,比如“在同一个事务中完成多行写入操作,而不是每写入一行都开启一个事务”等,也可以大大提升写入(查询)速度。优化无处不在,借鉴优秀的第三方框架,能够快速地学习如何写好代码。而扎实的基础是一切优化的前提。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值