Android数据库高手秘籍(十一)——LitePal支持事务功能了
# 前言
LitePal 内部的 API 在很早之前就支持了事务功能,因为要保证数据操作的原子性,不能出现部分成功部分失败的情况。
大家早上好,时隔两年之久,LitePal 今天终于又更新了!
是的,我看了一下时间,LitePal 的上个版本还是 2018 年 10 月份发布的,之后就再也没有更新过。因为我接下来将主要的时间都放在了 giffun 这个项目上,忙完 giffun 紧接着又开始编写《第三行代码》,以至于完全没有时间和精力去维护 LitePal。
期间有不少朋友咨询过我,是不是放弃维护 LitePal 了?莫名感到有点心酸,我欠这个项目的有点多了。
那么时隔两年之后的更新,LitePal 又发生了什么变化呢?我们一起来看一看吧。
# LitePal的变化
这两年时间里,我不光没有时间更新 LitePal 的功能,甚至连 GitHub 上的 issues 都无暇顾及,以至于积累了大量的 issues。那么在开发新功能之前,首先要做的,肯定是解决这些 issues。
我将所有的 issues 都浏览一遍之后,发现大体可以归为以下几类:
- 用法的咨询。对于这类 issue 我基本都进行了回复,只是回复的有点太晚了,可能没能帮上你们的忙,这里非常抱歉。
- 功能上的建议。这些年来许多朋友都在 LitePal 的功能性方面提供了不少建议,也让 LitePal 变得更加强大。不过关于功能建议方面的事情我待会还会再谈,这里暂时先跳过。
- 系统类型的 Bug。有些朋友使用 LitePal 时遇到了崩溃,就认为是 LitePal 的 bug,但有的时候并非如此。比如 CursorWindows 这个 bug 被提了好几次,但其实这是系统底层的限制,CursorWindow 缓存数据达到最大限制就会抛出异常。即使你不使用 LitePal,用原生的 SQLiteDatabase 也会出现这个异常,所以这种问题我确实无法修复,大家只能在使用层面尽量减少这种一次性加载大量数据的场景。
- 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'
}
复制代码
2
3
4
5
合二为一之后,大量冗余的代码就都可以删除了,维护成本也骤降了许多。至于是如何实现的,这主要得感谢 bintray-release 这个开源库(github.com/novoda/bint… (opens new window) 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 中已经引入数据库调试功能了),我最终还是会移除这个功能。
用过 LitePal 的朋友都知道,在 LitePal 当中向数据库存储一条数据是非常简单的,只需要调用如下代码即可:
Person person = new Person();
person.setXXX(...);
...
person.save();
复制代码
2
3
4
5
6
save 方法是 LitePal 提供的一个接口,它会解析当前对象中包含的数据、字段、关联关系等信息,然后将解析出来的数据存储到数据库表对应的列当中。
存储一条数据是上面这种写法,那么如果我要存储一个集合当中的数据应该怎么做呢?当然你可以这样写:
List<Person> personList = ...
for (Person person : personList) {
person.save();
}
复制代码
2
3
4
5
6
得到了一个集合之后,我们只需要循环遍历这个集合,调用每个 Person 对象的 save 方法就可以了。
但是刚才有提到,LitePal 的 save 方法中会解析当前对象包含的数据、字段、关联关系等信息。你会发现除了数据是会变化的之外,像字段、关联关系这种信息每个对象都是相同的,所以每次循环都去解析一遍这些信息无疑会增加存储耗时。
为此 LitePal 提供了一个 saveAll 方法,专门用于存储集合类型的数据,比如实现上述同样的功能,也可以这样写:
List<Person> personList = ...
LitePal.saveAll(personList);
复制代码
2
3
4
这两种写法实现的功能是一模一样的,但是 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()
复制代码
2
3
4
很明显,这种写法变得更加清爽了。
LitePal 内部的 API 在很早之前就支持了事务功能,因为要保证数据操作的原子性,不能出现部分成功部分失败的情况。
然而,LitePal 之前却从来没有提供过对外的事务接口,但是广大开发者却实实在在会有事务方面的需求。
举个最常见的事务例子,你正在开发一个转账功能,需要先从一个账户中减去先一定的金额,然后向另一个账户中增加相同的金额。整套操作必须保证是原子性的,即要么同时成功,要么同时失败。如果部分成功的话,转账之后,账户的总金额就对不上了。
为此,LitePal 3.1.1 版本当中终于加入了事务接口的支持,并且用法也十分简单,因为和 SQLiteDatabase 中提供的事务接口用法是几乎一致的。
当我们要进行一套数据库操作,并且要保证它们要么同时成功,要么同时失败,这个时候就可以这样写:
try {
LitePal.beginTransaction();
boolean result1 =
boolean result2 =
boolean result3 =
if (result1 && result2 && result3) {
LitePal.setTransactionSuccessful();
}
} finally {
LitePal.endTransaction();
}
复制代码
2
3
4
5
6
7
8
9
10
11
12
13
可以看到,这里调用 beginTransaction 方法来开启事务,调用 endTransaction 方法来结束事务,中间所有的数据库操作都是在事务当中的。如果所有的操作都成功了,那么我们可以在结束事务之前调用一下 setTransactionSuccessful 方法,这样所有的操作就都生效了。否则的话,所有的操作都会被回滚,就好像什么都没发生过一样。
事务的用法就是这么简单,然而在 Kotlin 当中,事务的用法会更加简单,因为我又提供了一个 Kotlin 专属的事务 API,写法如下:
LitePal.runInTransaction {
val result1 =
val result2 =
val result3 =
result1 && result2 && result3
}
复制代码
2
3
4
5
6
7
8
我来简单解释一下,我们可以给 runInTransaction 方法传入一个 Lambda 表达式,表达式中的所有代码就都是在事务当中运行的了,这种语法特性是利用 Kotlin 的高阶函数功能实现的。关于高阶函数上次我在直播的时候介绍得很详细,《第三行代码》也对这部分内容做了非常全面的讲解。
而 Lambda 表示式的最后一行要求返回一个布尔值,用于标识是否所有数据库操作都成功了,只有返回 true 的时候事务中的数据库操作才会生效,返回 false 或者中途发生异常所有的操作都会被回滚。
以上就是关于 LitePal 3.1.1 版本更新的所有内容,不过本篇文章是写给已经有 LitePal 基础的人看的,帮助他们快速地升级到 3.1.1 版本。如果你之前并没有接触过 LitePal,那么可以阅读我写的技术专栏《Android 数据库高手秘籍》 (opens new window),里面有非常详尽的 LitePal 使用讲解。
LitePal 的开源库地址是:
github.com/LitePalFram… (opens new window)
如果想要学习 Kotlin 和最新的 Android 知识,可以参考我的新书 《第一行代码 第 3 版》,点击此处查看详情 (opens new window)。
作者:郭霖 链接:https://juejin.cn/post/6986871870972755976 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。