文章出處

2016-03-16

Android數據庫支持

本文節選并翻譯《Enterprise Android - Programing Android Database Applications for the Enterprise》。
學習目標:

  1. 學習Android對SQL的支持。
  2. 理解在Java中使用SQL——通過SQLiteDatabase類。
  3. 創建數據庫——SQLiteOpenHelper類。
  4. 理解loaders、cursors和adapters。

為了在程序中使用本地的,結構化的數據,需要完成以下事情:

  1. 在java代碼中嵌套SQL命令,程序運行時執行它們。
  2. 根據需要創建,初始化,并升級數據庫。
  3. 選擇一種針對當前程序的數據庫生命周期管理策略。
  4. 解析查詢得到的數據,在程序中使用它們。

Java中執行SQL:SQLiteDatabase類

為了在java代碼中針對SQLite數據庫執行一些SQL查詢等操作,Android 框架提供了SQLiteDatabase類。通過獲得一個SQLiteDatabase對象實例,可以執行一些基本的,底層的數據庫操作。


以下使用db表示一個SQLiteDatabase對象

db.execSQL(String sql)

execSQL是一個 同步方法,它接收原始的SQL語句——那些可以在SQLite CmdLine中執行的SQL命令。方法執行完畢,SQL語句操作即執行完畢。

  • 那些以“.”開頭的命令只能在命令行執行,它們是sqlite3 命令行工具,不是execSQL可執行的SQL語句。
  • execSQL每次只能執行一個有效的SQL語句。
  • execSQL的執行不能返回任何數據,如果在這里傳遞一個query作為sql語句,那么會引起SQLiteException異常。

db.rawQuery(String sql)

rawQuery可以執行sql并返回Cursor作為結果:

Cursor c = db.rawQuery("pragma table_info(" + tableName + ")", null);

execSQL和rawSQL不應該作為代碼中執行SQL的一般選擇,應該盡量使用其它等價的SQL API來代替它們。execSQL的方便之處在于創建數據庫結構,通常來說rawSQL是完全應該避免使用的。
execSQL和rawSQL方法都接收bindArgs參數,方便在SQL中傳遞變量。避免SQL注入這樣的問題。

SQL語句的等價API

為了避免使用SQL字符串作為代碼中執行SQL的途徑——這需要良好的SQL知識,而且,很難像普通代碼那樣被調試和排查。Android SQLite API還提供了一系列的API來對應不同的SQL語法。包括insert、update、delete和query等,相應地,還有一些等價的簡化方法和數據庫管理方法。

delete

db.delete("pets", "age > 10 AND age < 20", null);
db.delete("pets", "age = ? OR name = ?", new String[] {"15", "linus"});

此方法是刪除數據的SQL的一個簡單拆分,比rawSQL略參數化些。

update

ContentValues newAges = new ContentValues();
newAges.put("age", Integer.valueOf(99));
db.update(
"pets",
newAges,
"name = ? OR name = ?",
new String[] {"linus", "fellini"});

類ContentValues提供了一組列名和值的綁定。

insert

ContentValues newPet = new ContentValues();
newPet.put("name", "luna");
newPet.put("age", 99);
db.insert("pets", null, newPet);

insert方法不拋出異常,返回-1表示失敗。而update和delete在違反關系數據庫的一些約束時會拋出SQLiteException表達執行錯誤。
可以使用insertOrThrow來主動拋出執行錯誤的異常。

replace

如果記錄不存在就insert,否則對已存在記錄執行update。

query

查詢方法是最復雜的一類數據庫操作,對應了一組API。一個完整的查詢SQL看起來如下:

SELECT table1.name, sum(table2.price)
FROM table1, table2
WHERE table1.supplier = table2.id AND table1.type = "spigot"
GROUP BY table1.name
HAVING sum(table2.price) < 50
ORDER BY table1.name ASC
LIMIT 100

查詢條件為空可以使用null來代替。query方法接收selection和selectionArgs兩個參數。前者可包含一些參數標記,后者是對應標記的實際值。
對應示例如下:

Cursor c = db.query(
"pets",
new String[] { "name", "age" },
"age > ?",// selection
new String[] { "50" },//selectionArgs
null, // group by
null, // having
"name ASC");

要對超過一個表進行聯合查詢,需要借助SQLiteQueryBuilder來構建對應的SQL,之后使用query方法執行此SQL即可。SQLiteQueryBuilder負責檢查對應的SQL語法錯誤,避免SQL注入。

外鍵約束和事務

SQLite默認不開啟外鍵約束,可以使用setForeignKeyConstraintsEnabled來開啟外鍵約束。但是不同API版本的行為和設置方式會有差異。同樣的,觸發器這樣的特性也不要過于依賴。最基本的,主鍵和列的唯一約束,自增等都是支持的。應該保持SQLite的輕量級和高效,可以在代碼中自行組合方法來完成約束的實現。
最后,SQLite對事務有完整的支持:

db.beginTransaction();
try {
// sql...
db.setTransactionSuccessful();
}
finally {
db.endTransaction();
}

SQLiteDatabase類提供的其它一些方法涉及到數據庫的刪除和創建,但是,使用SQLiteOpenHelper來完成對數據庫的管理是最好的選擇。

創建數據庫:使用SQLiteOpenHelper

在典型的網站后臺這樣的應用中,數據庫的設計和創建是一項獨立且完整的任務,這些過程更像是軟件部署的一個環節,而不是程序執行的一部分。
Android應用則是外全不同的情況,用戶下載并運行apk來安裝一個程序,其apk中包含所有相關的數據,安裝過程程序自身完成各種引導和設置。如果需要數據庫,程序自身負責創建它。而SQLiteOpenHelper類就是用來提供數據庫結構創建和升級的功能。
SQLiteOpenHelper是一個抽象類,它提供了一個創建數據庫需要的模板,對應每一個數據庫,都需要一個SQLiteOpenHelper的子類來完成對其的創建和升級。
當程序運行時,執行的代碼請求一個數據庫實例時,幫助類會檢查數據庫文件是否存在,不存在就創建對應名稱的數據庫文件,之后執行onCreate方法完成對數據庫結構(主要就是各種表)的初始化。

我們應該一直通過幫助類來獲得數據庫對應的SQLiteDatabase對象,因為它保證返回給我們的是完整、初始化好的、可使用的數據庫(這里指數據庫連接已打開)。最好不要自己的類中去使用字段持有一個SQLiteDatabase對象,Helper類提供了數據庫對象的創建,打開和關閉方法,自己維護的SQLiteDatabase對象對象很容易陷入一個廢棄、無法使用的狀態。

不要在onCreate中調用會直接或間接執行getReadableDatabase或getWriteableDatabase的方法or代碼。可以想象,這會陷入方法的循環執行。

數據庫版本

數據庫的onCreate方法接收一個大于0的int參數version作為對應數據庫的版本標識,作為數據庫的元數據。
幫助類在檢查數據庫的存在性時,同時會檢查數據庫的版本,如果當前的version參數和現有數據庫的版本號不一致,則根據大小關系執行onUpgrade和onDowngrade方法。
這兩個方法中可以對表結構進行調整,更重要的是,在數據庫表結構的變化過程中,自己的代碼需要盡可能根據需要保持用戶數據,避免丟失。這兩個方法的執行都是事務性的。
一個好的建議:使用alter table修改原表名,之后創建同名的新表(結構會有變化,但某些列是不變的),然后將數據拷貝到新表。

onConfigure和onOpen

一些情況下,數據庫是開啟了外鍵約束的,這會影響數據庫升級和降級的代碼邏輯。
可以使用以下兩個方法來達到暫時性的開啟和關閉外鍵約束這樣的目的:

  • onConfigure 方法在數據庫連接成功后立即執行——在onCreate、onUpgrade和onDowngrade方法的前面。此處執行setForeignKeyConstraintsEnabled會強制約束生效——對于數據庫的整個操作過程。
  • onOpen 方法在onCreate、onUpgrade和onDowngrade之后執行,使得這三個和數據庫結構創建和修改的方法的執行可以更自由和快速。例如像簡單的改表名這樣的操作,應該暫時無視外鍵約束。onOpen方法在數據庫結構完全初始化之后執行,那么此處執行setForeignKeyConstraintsEnabled方法,可以讓外鍵約束在數據庫結構初始化完成后才生效。

實際獲得一個數據庫對象的操作可能會很耗時,因為第一次的數據庫創建或升級會涉及到表的創建甚至數據的拷貝,所以需要注意這些操作的異步執行。相反的,SQLiteOpenHelper對象本身的創建是非常快速的。對應getReadableDatabase 和 getWriteableDatabase的執行會引起對實際數據庫對象的創建和獲取,使用loader可以完成對數據庫的異步訪問。

數據庫對象的管理

安卓應用程序在使用數據庫時,需要考慮對SQLiteDatabase對象的生命周期的管理。一個打開的數據庫對象大約占1KB內存。
數據庫對象的管理有以下2種策略:

  • 獲得并一直持有db對象(Get it and keep it)。
  • 僅在需要的時候獲取并使用db對象(Get it when you need it)。

一直持有db對象

這是一個很理想且簡單的db對象管理方式——除非有進程內存的限制考慮。當然,若對數據庫的訪問操作僅僅是整個程序中多個Activity中的個別在使用,那么顯然沒有必要一直保持著db對象。
當程序在作為后臺程序很長時間后,安卓系統會選擇殺死進程。那么,程序擁有的db對象、任何數據庫連接、以及任何程序進程相關的內存資源都會被釋放掉。(As
long as you’ve left the database in a consistent state — no uncommitted transactions and no open file
connections to large objects (BLOBs) — tweaking soon-to-be-deallocated memory is a waste of effort.)一旦你讓數據庫保持在這樣一個不變的狀態時——沒有任何未提交的事務,沒有任何對大對象文件的打開的連接時——去糾纏那些很快就會被釋放的內存顯然是沒必要的。
這個策略雖然簡單,還是需要注意:
如果代碼忘了顯式關閉db實例,那么GC僅僅是回收此對象,這樣會產生一個錯誤信息:

09-02 15:27:10.286: E/SQLiteDatabase(16433): close() was never explicitly called on
database '/data/data/net.callmeike.android.sqlitetest/databases/test.db'
09-02 15:27:10.286: E/SQLiteDatabase(16433): android.database.sqlite.
DatabaseObjectNotClosedException: Application did not close the cursor or database
object that was opened here
09-02 15:27:10.286: E/SQLiteDatabase(16433): at android.database.sqlite.SQLiteD
atabase.<init>(SQLiteDatabase.java:1943)
09-02 15:27:10.286: E/SQLiteDatabase(16433): at android.database.sqlite.
SQLiteDatabase.openDatabase(SQLiteDatabase.java:1007)
...
09-02 15:27:10.286: E/System(16433): Uncaught exception thrown by finalizer
09-02 15:27:10.297: E/System(16433): java.lang.IllegalStateException: Don't have
database lock!

上面問題的一個典型場景就是:在一個Activity中定義了字段來保持一個db對象的引用,當程序不可見——轉為后臺程序時,一旦Activity對象被GC,那么此db對象失去引用,也會被回收,我們無法再訪問它——也就無法去關閉db對象的連接了。
為了獲得并保持一個db對象,應該使用一個強引用來指向它。可以通過一個靜態變量或者是Application對象的變量來引用db對象。在Application對象中定義引用db對象的字段是很好的做法——這樣可以很方便實現在多個Activity之間共享此db對象。當然,直接將Application對象設計為單例模式來全局訪問也是可以的。

public class KeyValApplication extends Application {
private KeyValHelper dbHelper;
private Thread uiThread;
@Override
public void onCreate() {
super.onCreate();
// ...
uiThread = Thread.currentThread();
dbHelper = new KeyValHelper(this);
}
public SQLiteDatabase getDb() {
if (Thread.currentThread().equals(uiThread)) {
throw new RuntimeException("Database opened on main thread");
}
return dbHelper.getWriteableDatabase();
}
}

注意,不要在UI線程中執行實際打開數據庫連接的操作——它(很可能)是耗時操作。
dbHelper對象會創建并緩存準備好的db對象,正常情況下多次調用getWriteableDatabase和getReadableDatabase都返回的是同一個db對象,所以,我們沒必要自己“緩存”一個db對象,關閉db對象也應該通過dbHelper.close()方法來關閉。
在文件系統被占滿這樣的極端情況下,dbHelper只能返回給我們一個只讀的db,但當文件系統又有空閑的時候,dbHelper又會返回一個新的db對象——它是可讀寫的,之前的db對象被close并釋放掉。
所以,dbHelper完全負責我們要用到的db對象的創建、關閉和引用的釋放,我們自己的代碼中——也就是使用db對象執行操作的方法中,使用局部變量暫時持有db對象引用,或直接使用getDb()這樣的訪問器代替變量來獲得db對象——不要在自己的類中使用字段(成員變量)來引用獲得的db對象——你幾乎無法正確的維護它!
最好的做法就是一直使用getWriteableDatabase(它比getReadableDatabase更靈活,而且getReadableDatabase通常返回的就是同一個db對象)獲得db對象并直接使用,不要自己去維護它。

Cursor & Loader & Adapter

//待續...


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 AutoPoster 的頭像
    AutoPoster

    互聯網 - 大數據

    AutoPoster 發表在 痞客邦 留言(0) 人氣()