6

Android数据库高手秘籍(十一),LitePal支持事务功能了

 3 years ago
source link: https://guolin.blog.csdn.net/article/details/106462209
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。

大家早上好,时隔两年之久,LitePal今天终于又更新了!

是的,我看了一下时间,LitePal的上个版本还是2018年10月份发布的,之后就再也没有更新过。因为我接下来将主要的时间都放在了giffun这个项目上,忙完giffun紧接着又开始编写《第三行代码》,以至于完全没有时间和精力去维护LitePal。

期间有不少朋友咨询过我,是不是放弃维护LitePal了?莫名感到有点心酸,我欠这个项目的有点多了。

那么时隔两年之后的更新,LitePal又发生了什么变化呢?我们一起来看一看吧。

Close Issues

这两年时间里,我不光没有时间更新LitePal的功能,甚至连GitHub上的issues都无暇顾及,以至于积累了大量的issues。那么在开发新功能之前,首先要做的,肯定是解决这些issues。

我将所有的issues都浏览一遍之后,发现大体可以归为以下几类:

  1. 用法的咨询。对于这类issue我基本都进行了回复,只是回复的有点太晚了,可能没能帮上你们的忙,这里非常抱歉。

  2. 功能上的建议。这些年来许多朋友都在LitePal的功能性方面提供了不少建议,也让LitePal变得更加强大。不过关于功能建议方面的事情我待会还会再谈,这里暂时先跳过。

  3. 系统类型的Bug。有些朋友使用LitePal时遇到了崩溃,就认为是LitePal的bug,但有的时候并非如此。比如CursorWindows这个bug被提了好几次,但其实这是系统底层的限制,CursorWindow缓存数据达到最大限制就会抛出异常。即使你不使用LitePal,用原生的SQLiteDatabase也会出现这个异常,所以这种问题我确实无法修复,大家只能在使用层面尽量减少这种一次性加载大量数据的场景。

  4. LitePal的Bug。对于提出这类问题的朋友我非常感谢,这次确实又发现了几个LitePal内部的bug。比如特定情况下升级数据库会丢失数据、Date类型字段无法保存1970年以前的数据、findFirst()方法在某些时候查询速度会非常慢等等。这次在开发新版本之前,我将这些提出的bug全部都进行了修复,保证这是一个更加稳定的版本。

那么现在LitePal的GitHub中还剩下多少issue呢?给大家看一下:

20200531215724418.png

没错,就只剩下一个了。并且这是一个新功能的建议,我确实计划在之后的版本中考虑加入这个功能,所以暂时将它保留了下来。

好了,现在issues都解决掉了,接下来终于可以对LitePal进行升级了。

在之前的LitePal 3.0.0版本当中,我为了让它支持一些Kotlin中不错的语法特性,将原来的一个库变成了两个库,如下图所示:

20200531215748321.png

是的,使用哪种编程语言就引入哪个库,我本来认为这是一件很好的事情,然而没过多久我就后悔了,这是一个非常错误的决定。

将库分成了Java和Kotlin两个版本之后,它们又会共同引入Core库来作为依赖,Core库是主业务逻辑实现的地方。那么当需要添加什么新功能的时候,我需要在Core库中进行具体的功能实现,然后在Java库中添加一个对外接口,在Kotlin库中添加一个对外接口,还要为Kotlin的专属语法再添加一个对外接口。本来只需要在一个地方维护的代码现在变成了要在四个地方维护,所有API的数量也变成了四倍,导致代码维护成本急剧增加。

这个问题是我必须要解决的,不然以后LitePal会变得越来越难维护。所以,在最新的LitePal 3.1.1版本当中,已经不再区分Java版和Kotlin版,而是统一合并成一个库。只需要声明以下依赖库地址,即可将LitePal升级到3.1.1版本,Java和Kotlin语言都可以使用:

dependencies {
    implementation 'org.litepal.guolindev:core:3.1.1'
}

合二为一之后,大量冗余的代码就都可以删除了,维护成本也骤降了许多。至于是如何实现的,这主要得感谢bintray-release这个开源库(https://github.com/novoda/bintray-release)。它在将开源项目打包成库发布到jcenter之前,会先解析当前项目的依赖情况,然后将项目所需要依赖哪些库一起声明到pom文件当中。比如LitePal 3.1.1版本的pom文件如下所示:

20200531215807727.png

可以看到,这里在dependencies当中声明了LitePal是需要依赖Kotlin的一些运行时库的,如果你当前的项目中没有这些库(比如是使用Java开发的项目),那么Gradle会自动将这些依赖下载下来,以保证LitePal可以正常运行。

这样就不用再专门为Java和Kotlin提供两个版本的库了,而是一份代码同时兼容两种语言,皆大欢喜。

这里我想要回到刚才功能建议的话题。

LitePal从诞生一直到现在,其实都还算是一个比较小众的开源库。因为本身移动端数据库的需求就不是特别强,再加上LitePal也不是移动数据库框架中做得最出色的那个,所以不可能得到所有人的认可。

但是也有不少Android开发者,他们对LitePal特别喜爱,觉得这个库简单好用,可以省去编写好多代码。有一些热衷的朋友会向我提出很多建议,加入某某之类的功能,从而让这个库变得更加强大。

我特别感谢向我提出建议的这些朋友们,可以说在很大程度上,LitePal的版本迭代更新都是在你们的建议基础上进行的。

但是,迭代了这么多版本之后,我回过头来反思一下,是不是每一个建议都值得采纳呢?这是要打上问号的。

因为是一个小众开源库,建议本身可能就不太多,所以我很愿意听取,并在这些建议的基础上做加法。但是做了这么多年加法之后,我发现有些建议其实并不怎么合理,也不被大多数开发者所需要。加上这些功能之后,还会使得LitePal变得不稳定,或者是维护变得更加困难。

所以,这次我决定对LitePal做减法。

经过仔细思考之后,我决定分阶段砍去以下三部分内容。

1. 二进制数据存储

这个功能是我非常不应该增加的一个功能,因为数据库本身就不适合存储二进制数据。为什么呢?二进制数据通常都会很大,一张高清图片可能就会占据几M的内存,将这种数据存放到数据库中是比较危险的,很可能会引发刚才提到的CursorWindows的错误导致程序崩溃,这就让LitePal变得不够稳定。

那么又有多少开发者会有向数据库中存储二进制数据的需求呢?这个真的很少,因为大部分人的做法都是将二进制数据以文件的形式存储到本地,然后在数据库中存储一条文件的路径就可以了。这种做法更加科学安全,也不会给数据库增加额外的压力。

因此,从LitePal 3.1.1版本开始,将不再支持存储和读取二进制数据功能(实体类中定义的byte数组字段将被忽略),此项变更立即生效,如果有用到这部分功能的朋友,请在升级之前完成修改。

2. 异步操作

数据库操作需要异步进行,这个是一种非常提倡的行为,因为操作数据库本身就是比较耗时的。

然而,数据库操作需要异步进行,就意味着数据库框架需要提供异步功能吗?我以前是这么认为的,所以我在LitePal中加了很多异步操作的接口,不过现在我意识到,我又做错了。

因为除了数据库操作之外,有很多其他耗时操作也需要异步进行。异步这个话题展开来讲可以讲很深,也有极多的API和开源库可以用来实现异步功能,比如Java线程池、RxJava、协程等等。所以LitePal其实并不应该承担这个职责,有很多更适合的框架会专门处理这个事情。举个例子,Google的Room就完全没有提供异步操作数据库接口,但是默认情况下Room还强制要求你必须在非主线程进行数据库操作,否则就会崩溃。

另外,LitePal的异步操作接口设计得也确实非常不好,导致后期维护成本很高。比如说查询数据有一个find接口,那么为了可以异步查询数据,我就又提供了一个findAsync接口。删除数据有一个delete接口,为了可以异步删除数据,我就又提供了一个deleteAsync接口。大家发现问题了没有?为了提供异步操作,我将API的数量翻倍了,再加上之前又将库分为了Java和Kotlin两个版本,API在翻倍的基础之上又翻了四倍,维护成本指数级增加。

所以,在异步操作方面,我准备继续做减法,LitePal不再额外承担异步处理工作,但是也不会像Room那样强制要求开发者必须在非主线程操作数据库。到底是在主线程还是非主线程操作数据库,全凭大家自由选择。如果你们的项目中已经使用了RxJava或协程等技术,异步处理相信对于你来说本身就是一件很轻松的事情,也完全用不着使用LitePal提供的异步操作接口。

考虑到老项目的兼容性,此项变更并不会立即生效,目前只是所有的异步接口都被标记为了废弃,但在下一个版本当中将会完全移除,所以也请大家不要再继续使用这些接口了。

3. 数据库存储位置

LitePal在1.6.0版本当中,引入了将数据库存储到外置SD卡的功能,主要是为了方便大家调试程序。然而这种行为是极其危险的一种行为,会大大影响应用程序的安全性,因为谁都可以随意地更改数据库中的数据。

这个功能到底该去该留,我也考虑了很久。一方面是觉得,像Room这种Google官方的数据库框架都没有提供将数据库存储到外置SD卡的功能,LitePal为什么要多做这件事情。另一方面又觉得,数据库难以调试这确实是一个开发者的痛点。

深思熟虑之后,我决定暂时继续保留这个功能,但是随着未来开发调试环境越来越发达(比如Android Studio 4.1中已经引入数据库调试功能了),我最终还是会移除这个功能。

saveAll接口变化

用过LitePal的朋友都知道,在LitePal当中向数据库存储一条数据是非常简单的,只需要调用如下代码即可:

Person person = new Person();
person.setXXX(...);
...
person.save();

save方法是LitePal提供的一个接口,它会解析当前对象中包含的数据、字段、关联关系等信息,然后将解析出来的数据存储到数据库表对应的列当中。

存储一条数据是上面这种写法,那么如果我要存储一个集合当中的数据应该怎么做呢?当然你可以这样写:

List<Person> personList = ...
for (Person person : personList) {
	person.save();
}

得到了一个集合之后,我们只需要循环遍历这个集合,调用每个Person对象的save方法就可以了。

但是刚才有提到,LitePal的save方法中会解析当前对象包含的数据、字段、关联关系等信息。你会发现除了数据是会变化的之外,像字段、关联关系这种信息每个对象都是相同的,所以每次循环都去解析一遍这些信息无疑会增加存储耗时。

为此LitePal提供了一个saveAll方法,专门用于存储集合类型的数据,比如实现上述同样的功能,也可以这样写:

List<Person> personList = ...
LitePal.saveAll(personList);

这两种写法实现的功能是一模一样的,但是saveAll方法只会将Person对象中的字段与关联关系解析一次,因此存储效率将会大幅提升。

然而,saveAll方法也有一个缺点,就是如果存储的集合当中,有部分数据存储成功了,部分数据存储失败了怎么办?要知道,saveAll方法并没有返回值。

为了处理这种情况,LitePal 3.1.1版本当中特意增加了saveAll方法的返回值。

saveAll方法会返回true和false两种返回值,true表示集合中的所有数据都存储到了数据库当中,false表示存储过程中发生了异常,没有任何数据存储到了数据库当中。是的,saveAll方法内部开启了事务,要么全部存储成功,要么全部存储失败,不会出现部分存储成功的情况,这样可以避免很多使用saveAll方法时产生的误解。

另外,在3.1.1版本当中,我还为Kotlin提供了saveAll方法的专属语法糖,如果你的项目使用的正是Kotlin语言的话,可以用如下写法来调用saveAll方法:

val personList: List<Person> = ...
personList.saveAll()

很明显,这种写法变得更加清爽了。

LitePal内部的API在很早之前就支持了事务功能,因为要保证数据操作的原子性,不能出现部分成功部分失败的情况。

然而,LitePal之前却从来没有提供过对外的事务接口,但是广大开发者却实实在在会有事务方面的需求。

举个最常见的事务例子,你正在开发一个转账功能,需要先从一个账户中减去先一定的金额,然后向另一个账户中增加相同的金额。整套操作必须保证是原子性的,即要么同时成功,要么同时失败。如果部分成功的话,转账之后,账户的总金额就对不上了。

为此,LitePal 3.1.1版本当中终于加入了事务接口的支持,并且用法也十分简单,因为和SQLiteDatabase中提供的事务接口用法是几乎一致的。

当我们要进行一套数据库操作,并且要保证它们要么同时成功,要么同时失败,这个时候就可以这样写:

try {
	LitePal.beginTransaction();
	boolean result1 = // 数据库操作1
	boolean result2 = // 数据库操作2
	boolean result3 = // 数据库操作3
	if (result1 && result2 && result3) {
		LitePal.setTransactionSuccessful();
	}
} finally {
	LitePal.endTransaction();
}

可以看到,这里调用beginTransaction方法来开启事务,调用endTransaction方法来结束事务,中间所有的数据库操作都是在事务当中的。如果所有的操作都成功了,那么我们可以在结束事务之前调用一下setTransactionSuccessful方法,这样所有的操作就都生效了。否则的话,所有的操作都会被回滚,就好像什么都没发生过一样。

事务的用法就是这么简单,然而在Kotlin当中,事务的用法会更加简单,因为我又提供了一个Kotlin专属的事务API,写法如下:

LitePal.runInTransaction {
	val result1 = // 数据库操作1
	val result2 = // 数据库操作2
	val result3 = // 数据库操作3
	result1 && result2 && result3
}

我来简单解释一下,我们可以给runInTransaction方法传入一个Lambda表达式,表达式中的所有代码就都是在事务当中运行的了,这种语法特性是利用Kotlin的高阶函数功能实现的。关于高阶函数上次我在直播的时候介绍得很详细,《第三行代码》也对这部分内容做了非常全面的讲解。

而Lambda表示式的最后一行要求返回一个布尔值,用于标识是否所有数据库操作都成功了,只有返回true的时候事务中的数据库操作才会生效,返回false或者中途发生异常所有的操作都会被回滚。

我没学过LitePal怎么办?

以上就是关于LitePal 3.1.1版本更新的所有内容,不过本篇文章是写给已经有LitePal基础的人看的,帮助他们快速地升级到3.1.1版本。如果你之前并没有接触过LitePal,那么可以阅读我写的技术专栏《Android数据库高手秘籍》,里面有非常详尽的LitePal使用讲解。

LitePal的开源库地址是:

https://github.com/LitePalFramework/LitePal

如果想要学习Kotlin和最新的Android知识,可以参考我的新书 《第一行代码 第3版》点击此处查看详情

关注我的技术公众号,每天都有优质技术文章推送。

微信扫一扫下方二维码即可关注:

20181224140138240.jpg


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK