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


關注我的技術公衆號,每天都有優質技術文章推送。

微信掃一掃下方二維碼即可關注:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章