Android高效入門—SQLite資料庫

作者 | 2016-05-09

Android的SQLite資料庫

A4958

SQLite是一套開放原始碼的資料庫函式庫,並遵循ACID關聯式資料標準,使用標準的SQL語法,提供單機、無需連線環境的資料庫的管理系統。它被廣泛地使用在嵌入式系統、瀏覽器、作業系統內部。

Android支援SQLite資料庫,每個應用程式都可選擇建立自己的資料庫,將資料儲存在SQLite資料庫檔案中,開發人員使用Android提供的API類別庫即可在應用程式中存取資料庫中資料,包括查詢、新增、刪除與更新等標準的SQL存取語法。它亦提供應用程式訂定資料庫版本,可在新版應用程式一併進行資料庫版本的複製、轉移等工作。

一開始在Atm專案中建立一個具實感設計的活動,再進行必要的元件與畫面配置,待活動與畫面架構成形後,再進行SQLite資料庫的介紹與實作。因為,使用到資料庫的情境都需要使用者介面,筆者希望在設計操作資料庫的程式碼時,能與使用者的介面互動,並在練習過程中認識各個元件的角色與使用方法,單純地認識片段的資料庫操作程式碼,並不足以在設計應用程式時派上用場。

準備活動

建立一個新活動FinanceActivity進行範例與操作說明,這是一個簡單的記帳功能,可新增並顯示所有消費記錄,請在Atm專案中建立一個「Basic Activity」名稱為「FinanceActivity」,目的是顯示消費清單可在主畫面中按下「投資理財」功能時開啟這個活動,一開始是無任何內容,如下圖。

A4952

第二個活動是簡易的「Empty Activity」空白活動,名稱為「AddActivity」在此活動中設計新增消費記錄的使用者介面,供使用者輸入消費記錄,如下圖:

A4951

第一個消費清單為FinanceActivity,第二個新增消費為AddActivity,請先依以下步驟建立活動。

1.  建立活動

請使用功能表的「File/New/Activity/Basic Activity」建立具實感設計的基本活動,名稱為FinanceActivity,如下圖:

A4942

上述動作會產生一個FinanceActivity類別與兩個版面配置檔「activity_finance.xml」與「content_finance.xml」。

A4943

完成後,再使用功能表的「File/New/Activity/Empty Activity」新增另一個空白活動,名稱為AddActivity,如下圖:

A4944

上述動作產生一個AddActivity類別與一個版面配置檔「activity_add.xml」,如下:

A4945

2. 畫面配置

再打開產生的「res/layout/activity_add.xml」版面配置檔,加入必要的元件,預覽與元件結構如下圖:

A4946

配置檔activity_add.xml原始碼如下:

三個輸入方塊的id值為「ed_date」、「ed_info」與「ed_amount」,分別代表日期、消費說明與消費金額的輸入方塊,再為按鈕設定onClick屬性為「add」。

3, 實作必要程式碼

請再開啟AddActivity,設計以下程式碼:

上述程式使用前面章節介紹的方法,取得畫面中三項輸入元件,並將元件設定為屬性,最後加入新增按鈕的事件處理方法「add」。

AddActivity準備完成後,請開啟FinanceActivity,修改原本「onCreate」方法內浮動鈕的事件程式碼,將SnackBar產生程式碼換成

完成後,按下浮動鈕將開啟AddActivity活動畫面。

最後為了能在主功能畫面中按下「投資理財」功能時,可啟動FinanceActivity活動,請在MainActivity的onItemClick方法內的switch…case敍述中加入:

SQLiteOpenHelper類別

存取資料庫需要瞭解對檔案格式、協定與規範,以程式實作這些功能更是複雜,請不用擔心,Android提供一個已經實作好的-SQLiteOpenHelper類別,它的package是android.datatbase.sqlite。當應用程式需要使用SQLite資料庫時,只需要設計一個新類別並繼承SQLiteOpenHelper,這個類別即具有存取資料庫的能力。

建立應用程式的SQLiteOpenHelper類別

請展開專案「app/java/com.tom.atm」,在com.tom.atm套件上按右鍵選擇「New/Java Class」,名稱輸入「MyDBHelper」產生類別,再加入繼承「extends SQLiteOpenHelper」語法,如下:

A4377

1. 實作方法

由於SQLiteOpenHelper是抽象類別,其子類別必須實作必要的方法,請將游標停在出現錯誤的類別定義該行,按下「Alt+Enter」選擇「Implement methods」,如下圖:

A4378

對話框中已自動選擇onCreate與onUpgrade兩個方法,按下Enter即可,如下:

A4379

2. 實作建構子

因父類別SQLiteOpenHelper中並未實作無參數的建構子,因此MyDBHelper必須設計建構子,可將游標停在出現錯誤的類別定義該行,按下「Alt+Enter」選擇「Create constructor matching super class」建立與父類別相同參數的建構子,如下圖:

A4380

再選擇第一個即可,最後完成程式碼如下:

SQLiteDatabase類別

SQLiteDatabase類別的用途存取SQLite資料庫,提供許多能夠存取資料庫的方法,如query查詢、insert新增、update更新與關閉資料庫等方法,在這些方法中使用SQL語法並執行。

在SQLiteOpenHelper類別中可呼叫以下方法得到SQLiteDatabase物件:

  • getReadableDatabase()方法

讀取資料庫的SQLiteDatabase物件,可用在查詢。

  • getWritableDatabase()方法

擁有更新能力的SQLiteDatabase物件,用途為新增、修改或刪除。

請依實際開發時的用途,選擇呼叫上述兩個方法取得適合的SQLiteDatabase物件。

建立資料表格的時機-onCreate

當應用程式中執行到有關任何存取資料庫的指令時,假如資料庫檔案不存在,則會立即執行本方法。因此,在此撰寫建立資料庫表格的程式碼,若表格中需要初始資料時,在建立表格完成後可繼續撰寫新增資料記錄的程式碼。

在本例中每一筆消費記錄包括以下欄位:

  1. 消費日期
  2. 說明
  3. 金額

在表格中的辨識ID,請使用「_id」為欄位名稱,使其成為自動產生的主鍵(Primary key) ,表格欄位與資料型態如下表:

表格名稱:exp

欄位名稱資料型態備註
_idINTEGERPRIMARY KEY
cdateDATETIMENOT NULL
infoVARCHAR(20)
amountINTEGER

建立exp表格的SQL語法為:

在onCreate方法中,取得可寫入SQLiteDatabase物件後,呼叫execSQL方法建立exp表格,如下:

應用程式昇級-onUpgrade

當使用者手機中已安裝較舊版本應用程式時,在存取資料庫指令時自動檢查到舊的資料庫檔案時,會自動執行本方法。onUpgrade方法內可撰寫程式碼以協助更新使用者舊資料,以順利移轉至新版的資料表格。

使用SQLiteOpenHelper

完成了MyDBHelper類別設計後,請開啟AddActivity,準備設計新增消費記錄功能。

在MainActivity中的onCreate方法,呼叫MyDBHelper的建構子建立物件,物件名稱為「helper」,請將helper定義為屬性,如下:

MyDBHelper的建構子是與SQLiteOpenHelper相同的,參數與用法如下:

  1. Context context

此參數使用this關鍵字,即傳入AddActivity本身。

  1. String name

name為資料庫檔案名稱,由開發者自訂,可以使用任何副檔名,如「expense.db」或「expense.sqlite」。

  1. CursorFactory factory

在此使用null,代表以標準模式SQLiteCursor處理Cursor。

  1. int version

本應用程式目前資料庫版本,在此使用1代表第一個版本。

新增資料

使用SQLiteDatabase的insert方法新增記錄至表格,第一個參數為表格名稱,第三個則是「資料包」,使用ContentValues類別,就像是Java的Map集合類別,專門儲存Key-Value的一組對應資料組,其中Key鍵值使用欄位的名稱,Value則是該欄位的值,如下圖:

A4383

請在AddActivity的add方法中實作,將一筆消費記錄儲存在一個ContentValues物件中,最後再呼叫SQLiteDatabase的insert方法新增記錄,如下:

首先在第2-4行取得畫面上使用者輸入的資料,並在第5到8行產生並收集一筆記錄的集合,最後在第9行取得資料庫物件後呼叫insert方法,傳入表格名稱與values集合物件以新增這筆記錄,若成功會回傳新增記錄的id值,最後使用Log印出除錯資訊。

insert方法的nullColumnHack

假如資料表格中的所有欄位允許空值時,一般的SQL語法可使用如:「INSERT INTO 表格名稱」新增一筆欄位全是空值的記錄,但SQLite不允許這樣的語法,因此選擇一個欄位給予空值。insert方法中的第二個參數可填入一個欄位名稱,當第三個參數values內無任何資料時,會在該欄位上給予空值。這種情況不多見,讀者可直接給予第二個參數「null」值即可。

執行程式

執行後請在主畫面點擊「投資理財」功能後進入FinanceActivity的畫面,再點擊浮動鈕後開啟AddActivity,輸入幾項測試資料後按下「新增」按鈕,如下:

A4390

按下新增後,觀察LogCat中的記錄資料,出現如下,代表資料已成功新增到資料庫中:

A4955

回傳的整數1代表新增該筆記錄後,該筆記錄的ID值,請試著再修改資料後,加入幾筆供測試使用。

驗證與除錯

當新增記錄的程式碼執行時,Android系統發現SQLite資料庫檔案「expense.db」不存在,即自動呼叫MyDBHelper中的onCreate方法,建立expense.db檔案並執行onCreate中建立表格的程式碼。

如果執行專案並已新增至少一筆測試資料後,在模擬器中專案資料夾應已產生expense.db檔案,本節將使用Android SDK的「adb」工具,登入模擬器中驗證SQLite檔案與其內容。雖然本節內容與後續查詢、實務等並無關連與必要性,但開發人員應瞭解測試與驗證的方法,在未來出現問題除錯時,能擁有更完善的除錯能力。

1. 登入模擬器並切換目錄

2. 使用cd指令切換至應用程式目錄

ls指令可列出目錄下的檔案清單

如果上述ls的結果出現「databases」,這是SQLite檔案方置的目錄,代表新增記錄是成功的,請切換至該目錄下:

再使用ls指令列出檔案:

上述清單中出現expense.db,它就是SQLite檔案。

3. 檢查expense.db

接下來利用sqlite3指令可開啟SQLite檔案並進入SQLite管理介面,請使用「sqlite3 expense.db」指令:

上述指令執行後會進入SQLite介面,命令提示字元會改變為「sqlite>」在此符號下可執行兩類指令,一為SQLite指令,二為一般的SQL查詢、新增等語法,SQLite指令是以小數點開頭的指令,可使用「.tables」列出表格:

使用「.schema 表格名稱」可列出該表格建立的SQL語法,如下:

除了以小數點開始的SQLite指令外,可直接使用SQL語法,如查詢exp表格中的所有資料,使用SQL語法時,請記得語法的最後請加分號「;」,代表SQL語法的結束,遇到分號時才會開始執行,如下:

上述執行結果會以簡易的格式列出查詢結果,亦可手動以SQL的INSERT語法自行新增一筆測試資料,如下:

最後測試與驗證完成後,使用「.exit」SQLite指令離開管理介面,如下:

回到模擬器登入的命令提示字元,之後可使用「exit」再離開模擬器。

查詢資料

在Android中查詢SQLite資料庫時同樣透過SQLOpenHelper取得SQLDatabase物件後,呼叫rawQuery或query兩種方法,取得查詢結果「Cursor」物件。

Cursor

Cursor是一個Java介面,代表一個查詢後的結果,透過它的方法,可指向結果中的其中一筆記錄。移動至某一筆結果後,再使用getString、getInt、getLong等資料取得的方法,得到該筆資料的各個欄位值。

透過如getCount()方法取得目前查詢結果的筆數,不需要Cursor物件時,可呼叫close()方法關閉Cursor。本節範例將以實例介紹如何使用這些方法完成資料查詢的工作。

rawQuery方法

若開發人員很熟悉資料庫表格中的結構與欄位時,能夠完全以傳統SQL語法建立查詢時,可使用rawQuery方法並傳入SQL查詢字串,如下:

或在查詢語法使用「?」問號代表參數,並在第二個參數中使用字串陣列傳入參數,如下:

上述程式碼為查詢exp表格中符合_id欄位值為2的記錄。

若專案中的表格不複雜,或由一個人完全開發掌握的專案,可使用rawQuery方法查詢,唯日後擴充或維護時較為繁雜。

query方法

query的參數較多,一開始學習要記得參數用途,但完成的程式可讀性較高,query方法的參數規格如下:

上述的參數可區隔為三大部份,分述如下:

功能區隔參數說明
表格資訊相關String table欲查詢的表格名稱
String[] columns查詢結果的欄位
條件相關selectionSQL的WHERE語法,只需「WHERE」後的查詢字串
String[]selectionArgs若前一個參數selection字串中有問號(代表參數)時,以此字串陣列對應問號所代表的值
其他附加查詢String groupBySQL的GROUP BY語法
String havingSQL的HAVING語法
String orderBy排序語法,給予null值為預設由小到大排序,使用「DESC」則是由大到小排序
  • 查詢exp表格所有資料

除了表格名稱,後面的六個參數全部給null,代表所有欄位。

  • 查詢exp表格中_id值為3的資料

WHERE語法使用「_id=?」字串,代表_id值為問號,問號代表由下一個參數字串陣列取得,第一個問號值由字串陣列的第一個值(index為0)提供,以此類推,程式範例如下:

  • 查詢exp表格中info與amount欄位,amount由大至小排列

第二個參數為兩個欄位名稱的字串陣列,最後排序參數給予「DESC」字串,如下:

實務範例

筆者將在消費功能中實作FinanceActivity,使其在應用程式開啟時,即查詢exp資料表格,並將查詢的結果以「ListView」清單元件展示。一開始的範例只需顯示兩個欄位,不需要設計ListView欄位的版面配置,最後範例會修改成顯示四個欄位,包括id值、日期、說明與金額,此時才需要自行設計符合客製化的清單項目版面配置檔。

1. 準備資料

為了本例查詢結果能顯示清單的效果,請先利用本章已完成的新增消費的功能,先新增幾筆測試資料。

2. 準備元件

請打開「content_finance.xml」,加入一個「ListView元件」並設定其ID值為「list」,原始碼如下:

使用SimpleCursorAdapter

在先前章節介紹清單時,有關Adapter曾介紹ListView的資料來源若是查詢結果-「Cursor」物件時,可使用SimpleCursorAdapter建立Adapter物件,其建構子規格如下:

  1. Context context

第一個參數為Context物件,傳入Activity的this關鍵字即可。

  1. int layout

在清單元件中的一列項目的版面配置資源,若顯示資料為一欄或兩欄,可使用Android SDK資源,如「android.R.layout.simple_list_item_1」或「android.R.layout.simple_list_item_2」,如果多於兩欄或想客製化項目的長像,可在專案中自行新增Layout檔,使用資源值「R.layout.資源名稱」。

  1. Cursor c

經過查詢語法得到的Cursor物件。

  1. String[] from

資料來源的欄位名稱字串陣列,查詢結果Cursor物件中的欄位資料。

  1. int[] to

畫面中的資源ID值陣列,如果使用版面配置檔是「android.R.layout.simple_list_item_2」,則使用該檔中的元件ID值「android.R.id.text1」與「android.R.id.text2」。

  1. int flags

這個參數和ListView更新畫面中的項目資料有關,如果給「0」,代表ListView在展示過程中資料庫中的記錄如果被更動了,ListView將不自動重新查詢並更動畫面中的資料。

實作步驟如下:

  1. 先取得FinanceActivity中的ListView(id為list)

  1. 產生本章所設計的「MyDBHelper」物件,並查詢exp表格

先用同樣方法呼叫MyDBHelper建構子得到物件再查詢,最後得到Cursor物件。在此需小心在不同類別(如Activity)中建構SQLiteOpenHelper物件的問題,應在MyDBHelper類別設計中採用單一物件的設計模式(Singleton),本章的最後將說明重構與設計方式。

  1. 最後產生SimpleCursorAdapter物件

呼叫SimpleCursorAdapter的建構子產生物件,並設定在list清單物件中,完整程式碼如下:

 

第10行,呼叫建構子,傳入第一個參數Context,使用this代表FinanaceActivity。

第11行,接著傳入Android SDK中的版面資源「android.R.layout.simple_list_item_2」代表一列資料中顯示兩個TextView。

第12行,再傳入第8行所查詢的Cursor物件c。

第13行,為顯示「info」與「amount」兩個欄位。

第14-15行,則是兩個欄位所對應版面中的id值與flags參數。

第16行,最後將adapter設定給list清單元件。

 執行結果

每筆記錄顯示info與amount兩個欄位,以Android常用的上下兩欄方式展示,如下圖:

A4956

客製化的欄位配置

若資料欄位大於兩個或需自行設計時,請先點擊專案的「app」後,使用功能表「New/Android resource file」,設計新的版面資源,如下:

1. 設計項目版面配置資源

File name: 輸入finance_row.xml

Resource type: 選擇Layout

如下圖:

A4957

設定LinearLayout排列屬性orientation為水平(horizontal),再放入四個TextView元件,如下圖:

A4400

2. 訂定每個元件的id值

四個TextView分別設定id值為「item_id」、「item_cdate」、「item_info」與「item_amount」,樹狀圖如下。

3. 以權重方式分配元件寬度

為了在ListView的各個欄位都能對齊,筆者使用LinearLayout常用的權重配置方式,先將所有元件的寬度屬性都設為「0dp」,再分別為元件設定權重值為「1,2,1,1」,最後的row.xml原始碼如下:

4. 更換SimpleCursorAdapter建構程式碼

最後回到MainActivity的onCreate方法中,將原本使用兩欄的程式碼更改如下:

第2行,改用專案資源R.layout.finance_row。

第4行,資源來源的欄位選擇四個都顯示。

第5-6行,將四個欄位對應R.layout.finance_row畫面中的四個元件id值。

執行結果

執行後畫面上每列顯示四個欄位,並以權重分配其寬度且各列對齊,如下圖:

A4958

SQLiteOpenHelper的Singleton設計

本章範例雖然在不同的Activity中都產生MyDBHelper物件,雖然執行都正常,但當專案需求越複雜時,很可能在同一時間存取到同一個SQLiteDatabase物件時會出現資料庫被鎖住的問題。因此,較簡單的方式是將MyDBHelper設計成單一物件,利用「static」的特性,確保在整個應用程式中都使用同一個MyDBHelper物件,也就是設計模式的「Singleton」模式。

在MyDBHelper類別中新增一個封閉的static類別變數:

再設計一個公開的getInstance()方法,以取得MyDBHelper物件,最後再將原本的建構子的修飾字由public改為private,程式碼如下:

完成後,再到FinanceActivity與AddActivity將原來產生MyDBHelper物件的程式碼由:

改為:

經過Singleton後的MyDBHelper與Activity類別,能夠確保同一時間只有一個MyDBHelper實例在運行,可避免存取資料庫的死結(dead lock)的問題。

專案設計到此時,大致上的功能是完成了,只是介面上還需要再改進,如日期、說明等輸入的方式。請參考延續本篇的元件使用文章:

Android的日期選擇元件DatePicker—SQLite延伸內容1

相關文章:

Category: Android SQLite 標籤:, , , , , ,

關於 Hank Tom

專長為程式語言、雲端服務開發,Linux系統管理, 任職:利拓科技 技術長,海林行動科技 技術總監 輔仁大學 兼任助理教授 ,為 Android高效入門>深度學習、CentOS 7建置、管理與伺服器架設實戰、Java網路程式設計、雲端網頁程式設計-Google App Engine應用實作 等書作者

使用Facebook直接回應

10 thoughts on “Android高效入門—SQLite資料庫

  1. Morton

    老師好:
    因照著做有發現報錯,在onCreate那邊的getWritableDatabase().execSQL ,我把它改成db.execSQL才能建立,再麻煩你確認一下,謝謝~

    回覆
    1. Hank Tom 文章作者

      您好,這是個問題,應使用方法中的db物件去執行才對,這是內容的錯誤,謝謝。

      回覆
  2. yuren

    02-09 07:56:33.369 2584-2584/com.example.user.atm D/ADD: -1
    請問我的是-1是什麼意思?

    回覆
  3. Kuang

    請問為什麼 table_name要用 main.exp? 不能直接用 exp就好了嗎

    回覆
    1. Hank Tom 文章作者

      每個DBMS都會有Database後,才有Tables,main就是SQLite中使用的預設Database名稱,有加沒加都可以

      回覆
  4. Code

    老師,我照著做 發現回傳的是-1,於是我把下面這行
    values.put(“into”, info);
    改成
    values.put(“info”, info);
    就正常了,不知道是不是內容錯誤

    回覆
  5. Soar

    老師好:
    因照著做,發現到好像多打了一個 . 在db.execSQL();這句裡面

    回覆

發表迴響

你的電子郵件位址並不會被公開。 必要欄位標記為 *