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

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

大家早上好,时隔两年之久,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呢?给大家看一下:

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

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


合二为一

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

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

将库分成了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文件如下所示:

可以看到,这里在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


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

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

猜你喜欢

转载自blog.csdn.net/sinyu890807/article/details/106462209