使用本地端資料庫來讀存資料-SQLite

廖時賢
Dec 7, 2020

--

在開發APP的時候,如何讀存各種資料是一個重要環節,各式各樣的資料庫相信各位開發者們就算沒用過也有聽過,從SQL-like的關聯式資料庫如MySQL、MS SQL或Oracle資料庫,到NoSQL資料庫如MongoDB、Redis等,要怎麼在資料庫與用戶端之間做連結也是一門學問,這也讓有些剛起步的開發者遇到一些瓶頸;另一方面,有時開發的用戶端APP不需要有一個"共享"的資料庫,每個使用者可以使用自己個人的資料就好;或者為了提供更好的使用者體驗,在每次APP開啟時先從本地端資料庫讀取資料並呈現,之後再透過網路連結遠端資料庫,更新本地端資料;針對上面的這些狀況Google在開發者文件中提供了些解決方案,也就是這篇文章要介紹的SQLite以及下一篇會介紹的Room。

P.S.為了不讓篇幅過長,本篇文章不會介紹到資料庫相關概念,如果沒有相關概念的讀者再麻煩先自行google囉😅

簡介

首先先來介紹SQLite,SQLite是一套由C語言架構出來的Library,其中實作了SQL資料庫引擎,而Android這個框架式由C語言建構出來的,因此遠從Android API 1時就已經內建SQLite這個Package,因此不需要添加任何依賴直接就可以使用

建立資料庫

繼承SQLiteOpenHelper

要使用SQLite之前,需要先建立一個類別繼承SQLiteOpenHelper,這時會跳出錯誤訊息要求要覆寫onCreate()和onUpgrade()兩個方法,覆寫方法的部分後面再講,這邊先來看看建構子,SQLiteOpenHelper有3個不同的建構子,這邊用的是最簡單的

SQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version)

第一個參數context就是在甚麼情境下需要使用到這個資料庫,因此從自訂的DBHelper中傳過來,這樣當呼叫DBHelper時一併把context傳進SQLiteOpenHelper;第二個name是指資料庫名稱,可以在單一個APP中同時使用多個不同資料庫(但是不建議這樣做,除非真的有需要用到同樣Table名稱來儲存不同的資料),這時可以透過資料庫名稱來知道接下來是要操作哪個資料庫;接著的factory老實說我不是很清楚甚麼情況下會需要自訂CursorFactory(如果有路過的大大有用過的,再麻煩留言告知,感謝!!),但這邊傳入null代表使用預設的SQLiteCursor;最後的version則是資料庫版本號碼,當更新資料庫的schema時記得要更新版本號碼資料庫才會知道要更新(呼叫onUpgrade()方法)。

資料庫模式(Schema)

接著講Schema,SQL-like的資料庫是由多個Tables組成,而每個Table又會有幾個不同的Columns,而在Google開發者文件中有提到,為了讓整個程式更加系統化,建議是用途比較接近、可能會需要一起用到的Tables用一個Contract類別包起來,之後再用inner class來區分每個Table,這樣在不同的layout中也可以schema是一樣的;舉例來說,假設現在有幾個畫面會呈現文章相關資料,而這包含作者資料、文章內容,那依照建議會這樣實作

創建完必要的Entries後,需要透過SQL statement來真的建立出這些Tables,因為現在需要建立兩個Tables,因此需要兩個SQL statement

SQLite2.X可以用的資料型態有4種

  1. NULL:用來儲存空值(null)
  2. INTEGER:用來儲存整數值,而Bolean也用1(true)和0(false)來儲存
  3. TEXT:用來儲存字串
  4. REAL:用來儲存小數

而在SQLite3中又新增了一個BLOB型態,用來儲存大型物件;而在創造Tables的statement外,我們也需要兩個statement來移除Tables

覆寫onCreate()和onUpdate()

接著回到需要覆寫的兩個方法,首先在onCreate()中會把需要用到的資料表建立起來

而要如何覆寫onUpdate()就需要看使用情境了,以Android開發人員文件中的範例來說,因為這個SQLite只是一個遠端資料庫的暫存資料,所以更新時可以直接把全部的資料清除掉然後再重新建立一個新的資料庫;但如果是像這篇文章中,需要更新資料庫架構,同時又保留原本的資料,則可能需要比對版本號來處理相對應的SQL;這邊以開發人員文件中範例來做示範。

到這邊前置作業算完成,接下來就可以在context中使用資料庫囉!

在context中使用資料庫

在需要用到資料庫的context(通常是Activity)中,將目前的context傳到剛剛建立的DBHelper類別中,這樣一個儲存在本地端的資料庫就建好囉!

接著呼叫要使用的資料庫類型

可以看到在這邊有readableDatabase和writableDatabase可以選擇,而在大部分情況下,這兩個方法得到的會是同樣的一個物件(可以讀取&寫入資料),只有在某些情況下-比如資料庫可使用的記憶體空間滿了-呼叫readableDatabase會得到一個只能儲存的資料庫,而呼叫writableDatabase則會報錯,因此還是建議根據不同的使用需求,來呼叫不同方法得到資料庫。

操作資料庫 (CRUD)

資料庫可以使用了,接著就是對於資料庫的操作了,也就是常聽到的CRUD

Create,新增資料
Read,讀取資料
Update,更新資料
Delete,刪除資料

Create

在SQLite中使用insert()方法來新增資料,insert()方法有3個參數,依序是

table(String):要新增紀錄的Table
nullColumnHack(String):Table中任意一個可空欄位,也可傳入null
values(ContentValues):要新增的資料

先看範例,假設現在要存入一筆作者的資料

當成功新增資料後,資料庫會回傳該筆資料的ID;關於nullColumnHack,他的作用在於當傳入的ContentValues沒有輸入資料時,會在資料庫中建立一筆空的紀錄;而如果像上面例子中傳入null值,則當ContentValues中沒有資料時就不會在資料庫中建立紀錄;為什麼要這樣做呢?這就牽扯到SQLite底層的問題,相關探討可以看這個回答;另外如果是下面2種狀況同樣不會建立空資料:

  1. 傳入的是不可空欄位
  2. 雖然傳入可空欄位,但資料表中有不可空欄位且沒有預設值

Read

SQLite中讀取資料用的是query()或者rawQuery()方法,個人覺得如果懂SQL query的話還是用rawQuery()方法會比較方便,不過如果沒那麼熟悉的就用query()吧;這邊講一下query()方法最多參數的多載方法並依序介紹每個參數的用處,實際使用上可能不需要用到全部的參數,則可以參考文件中的其他多載方法囉

distinct(boolean):是否回傳重複紀錄,true時只會回傳不重複的紀錄
table(String):想要讀資料的Table
columns(String[]):讀取哪些欄位資料
selection(String):回傳的紀錄符合哪些搜尋條件,如果傳入null則回傳全部記錄
selectionArgs(String[]):如果有設置selection,則依序由selectionArgs中的值來替換selection中的?
groupBy(String):回傳紀錄根據定義的欄位來分組,傳入null則記錄不分組
having(String):當有分組時,只回傳符合條件的分組,傳入null則回傳全部分組
orderBy(String):回傳資料根據哪些欄位來做排序,傳入null則不排序
limit(String):限制回傳資料筆數,傳入null則回傳全部資料
cancellationSignal(CancellationSignal):傳入一個CancellationSignal,當SQL操作進行中時,可以透過這個signal物件來取消操作(cancel())、取得操作是否被取消的狀態(isCanceled())、當操作被取消時要做甚麼事情(setOnCancelListener())以及當操作被取消時丟出例外(throwIfCanceled())

接著來看範例,假設目前資料庫中有3筆作者的資料

如果現在要得到"全部名字是David的作者資料",則先透過query()方法得到Cursor物件

可以把Cursor物件視為搜尋結果的集合,所以只要確認Cursor內還有下一個物件(透過moveToNext()方法),就可以得到每筆資料;但Cursor讀取欄位的方法比較麻煩,需要先定義得到的資料型別,再將要得到的欄位Index傳入,所以如果要把每位作者的姓名印出來,需要像下面這樣

將每筆作者姓名印出
結果

當資料讀取完,記得呼叫close()方法將cursor使用的資源釋放。

Update

如果需要更新資料則透過update()方法,這個方法有4個參數,依序是

table(String):要更新哪個Table的紀錄
values(ContentValues):要更新的欄位和值
whereClause(String):符合哪些條件,如果傳入null則更新該Table的全部紀錄
whereArgs(String[]):如果有設whereClause,則依序由whereArgs中的值來替換whereClause中的?

一樣看範例,假設David Jackson改名叫Tom Jackson,那資料庫中本來是David Jackson的資料要更改,假如知道那筆資料的ID,則直接透過ID來搜尋並更改即可;但如果不確定ID,則保險點的方法是將姓、名全部都當作搜尋條件後再做改動

透過ID來更改資料
透過姓、名來更改資料

不管用哪種方法都可以得到相同的結果

而update()方法回傳值是代表受影響更新的紀錄筆數,因此上面的SQL statement得到的回傳值是1;如果只搜尋first_name = "David" 來更新資料的話,得到的回傳值就會是2(本來有兩筆紀錄的first_name是David),同時得到的結果會變成

Delete

最後一個對資料庫的操作就是刪除資料,使用的是delete()方法,這個方法有3個參數

table(String):要刪除資料的Table
whereClause(String):符合的條件,傳入null則刪除Table中全部紀錄
whereArgs(String[]):如果有設置where,則依序由whereArgs中的值來替換whereClause中的?

上範例,假設要刪除掉David Martin的資料,那程式碼會這樣寫

而資料會從原本的3筆紀錄

變為2筆

2的David Martin被刪除了

跟update()一樣,delete()方法回傳的是符合條件被刪除的紀錄筆數,因此上面範例會得到回傳值1。

結論

關於SQLite在Android中的最基本使用方法大概是這樣,其他比如insert和update都還有xxxWithOnConflict()方法,可以決定當要新增或更新的資料有衝突時要怎麼處理;或者透過rawQuery()方法,直接使用SQL statement來獲得資料,這些比較深入的方法礙於篇幅就不在這篇文章做介紹,建議可以透過Android開發者文件來了解要怎麼用,當會使用的方法越多,則對於資料的操作上就能夠更加靈活。

而除了原生的SQLite以外,其實Google也有提供另外一個解決方案,在下篇文章中會介紹到目前在Android開發中更熱門的套件-Room。

參考資料

SQLite homepage
Save data using SQLite (Android developer doc)
Android SQLite onUpgrade方法的運用

--

--

廖時賢
廖時賢

Written by 廖時賢

Android/React Native developer. Start coding by interesting and become a job now.

No responses yet