讀古今文學網 > Android程序設計:第2版 > 使用數據庫API:MJAndroid >

使用數據庫API:MJAndroid

在本節中,我們提出了名為MJAndroid的更高級的示例應用,它演示了在一個虛擬的職位搜索應用中小型數據庫的使用。本章我們將探討該程序的數據庫持久性方面。在第15章,我們將查看這些應用如何集成映射功能,並在地圖上顯示職位查詢結果。首先,稍詳細地解釋該應用。

Android和社交網絡

Android手機的一個很大的前景是它們運行的應用可以提高用戶之間的社交能力。該前景反映了互聯網現實——第一代互聯網應用是用戶訪問信息,很多這種應用已經很流行了。互聯網應用的第二個浪潮是把用戶互相連接起來。如Facebook、Youtube及很多其他這樣的應用,以促進有相似愛好的人之間的連接,並支持應用用戶提供應用的部分或所有內容。Android有潛力吸納這個思想,並增加新的維度——移動。人們認為全新一代的應用會為移動設備用戶構建:社交網絡可以在人們漫步街道時都很容易使用它,可以知道用戶的地理位置,可以支持如圖片和視頻這樣的富信息並輕鬆共享等。MJAndroid給出了Android在這方面不斷努力的一個具體例子。

在MJAndroid MicroJobs應用中,有個用戶嘗試在其地理位置附近找個臨時工作,她可以在那裡工作幾小時,獲取一些額外的收入。前提是想要找臨時工的僱主輸入了空缺職位、描述和時間,並基於Web數據庫提供薪酬,這些職位信息可以在Android手機上獲取。尋找小時工的人們可以使用MicroJobs應用來訪問該數據庫,在其附近查找職位,和朋友交流潛在的僱主和職位,如果感興趣的話,直接給僱主打電話。在這裡,我們不想創建在線服務,而只是想要保存一些手機上的封裝好的數據。該應用包含很多特徵,它們擴展了移動設備獨有的核心思想。

映射(mapping)

Android手機環境提供了對動態、交互式地圖的支持,我們將充分利用其功能。在P392「MapView和MapActivity」一節中,會看到通過非常少的代碼,就能夠顯示本地社區的動態地圖,從內部GPS中獲取地理位置更新,在運動時自動滾動地圖。我們能夠在兩個方向放大和縮小滾動地圖,甚至是切換到衛星視圖。

尋找朋友和事件

在第15章,將看到在地圖上存在圖形疊加,將顯示在該區域哪個地方有空缺職位,並且只需要輕輕觸摸地圖上的符號就可以獲取關於工作的更多信息。我們將訪問Android的聯繫人管理器應用來獲取朋友的地址信息(電話號碼、即時消息等),以及訪問MicroJobs數據庫獲取更多發佈的職位信息。

即時通信

當我們找到想要聊天的朋友時,就可以通過即時消息聯繫他們。

和朋友或僱主聊天

如果短信方式太慢或太麻煩,則可以很輕鬆地給朋友打電話,或者給提供職位的僱主打電話。

瀏覽Web

大部分僱主有個關聯的Web站點,其上提供一些詳細的信息。能夠從列表或地圖中選擇一名僱主,並快速登錄其網站瞭解一下情況。

這是一個很有趣的應用,可以進一步把它開發成一個功能完善的服務,但是本書的目的是要說明在自己的應用中開發和集成這些強大的功能是多麼容易。本書的所有代碼都可以下載。雖然要瞭解本書的內容不一定要下載這些代碼查看,但是強烈建議你把這些源代碼下載到計算機。這樣你就可以很方便參考,而且很容易查看部分代碼並粘貼到你的應用中。現在,我們將使用MJAndroid示例,提供「接近現實」的示例,來深入說明Android數據庫API。

圖9-3顯示了當第一次運行MJAndroid時的屏幕顯示。它是本地區地圖,包含幾個按鈕。

圖9-3:MJAndroid打開屏幕截圖

源文件(src)

MJAndroid的包名是com.microjobsinc.mjandroid。Eclipse展開類似的路徑結構,正如對於任何Java項目那樣,當打開src目錄時會顯示全部內容。除了這些包文件夾,還有一個文件夾,包含項目的所有Java文件。這些文件包括:

MicroJobs.java

該應用的主要源文件——首先開始啟動的活動,顯示應用的核心地圖,並調用實現用戶界面所需要的其他活動或服務。

MicroJobsDatabase.java

它是一個數據庫助手,能夠幫助輕鬆訪問本地MJAndroid數據庫。使用SQLite在這個文件中存儲所有的僱主、用戶和職位信息。

AddJob.java和EditJob.java

它們是MJAndroid數據庫的一部分。提供用戶可以用於增加或編輯數據庫中的職位信息的界面。

MicroJobsDetail.java

顯示關於某個特定職位的所有詳細信息的Activity。

MicroJobsEmpDetail.java

顯示關於僱主信息的Activity,包括名字、地址、聲譽、郵件地址和電話號碼等。

MicroJobsList.java

顯示職位列表的Activity(和MicroJobs.java中的地圖視圖不同)。它顯示了簡單的僱主和職位列表,支持用戶通過任意字段對列表進行排序,以及通過觸摸列表上的名字打電話咨詢關於職位或僱主的具體信息。

載入和啟動應用

從SDK運行MJAndroid很複雜,因為應用使用MapView。當使用MapView時,Android需要特殊的地圖API密鑰,密鑰和特定的開發機關聯。在P138「Google地圖API密鑰」一節中,我們已經瞭解了簽名和啟動應用的需求,因為該應用依賴於地圖API,故需要設置API密鑰,示例才能正常工作。要啟動MJAndroid,打開和運行本章給出的Eclipse項目,正如你在其他章節所執行的。

數據庫查詢及從數據庫中讀取數據

存在很多種方法可以從SQL數據庫中讀取數據,但是它們都回到基礎的操作順序:

1.創建SQL語句,描述需要檢索的數據

2.在數據庫上執行該語句

3.把結果SQL數據映射到可以理解的數據結構中

對於對像-關係映射軟件,這個過程可能是非常複雜的,或者直接在應用中編寫查詢,它相對簡單但是需要很大的工作量。對像關係映射(ORM,http://en.wikipedia.org/wiki/Object_relational_mapping)工具把數據庫編程代碼隱藏起來,通過對像映射避免了字段的複雜性。從處理數據庫變化角度看,代碼可能更健壯,但是需要更複雜的ORM設置和維護。目前,在Android應用中使用ORM不是很普遍。

直接在應用中編寫SQL查詢只對於非常小型的項目可行,它不會隨著時間變化。直接包含數據庫代碼的應用會增加代碼脆弱的風險,因為當數據庫模式發生變化時,必須審查或可能重寫引用該模式的任何代碼。

常見的折中方式是把所有的數據庫邏輯轉換成一組對象,其唯一目的是把應用請求轉化成數據庫請求,並把結果返回給應用。在MJAndroid應用中我們採用的就是這個辦法:所有數據庫代碼都包含在MicroJobsDatabase類中,該類繼承自SQLiteOpenHelper。但是有了SimpleFinchVideoContentProvider,數據庫就變得非常簡單,我們不需要使用額外的字符串。

如果不使用內容提供者,Android支持使用定制的游標,可以在定制的游標內隱藏每個特定數據庫的操作的所有信息來進一步減少代碼依賴性。代碼中首先是MicroJobsDatabase的getJob接口調用。該方法的目標是要返回JobsCursor,它包含從數據庫中獲取的職位。用戶可以選擇(通過傳遞給getJobs方法的參數)通過title列或employer_name列對職位進行排序:


public class MicroJobsDatabase extends SQLiteOpenHelper {
...
    /** Return a sorted JobsCursor
     * @param sortBy the sort criteria
     */
    public JobsCursor getJobs(JobsCursor.SortBy sortBy) {
1
        String sql = JobsCursor.QUERY + sortBy.toString;
2
        SQLiteDatabase d = getReadableDatabase;
3
        JobsCursor c = (JobsCursor) d.rawQueryWithFactory(
4
            new JobsCursor.Factory,
            sql,
            null,
            null);
        c.moveToFirst;
5
        return c;
6
    }
...
    public static class JobsCursor extends SQLiteCursor{
7
        public static enum SortBy{
8
            title,
            employer_name
        }
        private static final String QUERY =
            "SELECT jobs._id, title, employer_name, latitude, longitude, status "+
            "FROM jobs, employers "+
            "WHERE jobs.employer_id = employers._id "+
            "ORDER BY ";
        private JobsCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
            String editTable, SQLiteQuery query) {
9
            super(db, driver, editTable, query);
        }
        private static class Factory implements SQLiteDatabase.CursorFactory{
十
            @Override
            public Cursor newCursor(SQLiteDatabase db,
                    SQLiteCursorDriver driver, String editTable,
                    SQLiteQuery query) {
⑪
                return new JobsCursor(db, driver, editTable, query);
⑫
            }
        }
        public long getColJobsId{
⑬
            return getLong(getColumnIndexOrThrow("jobs._id"));
        }
        public String getColTitle{
            return getString(getColumnIndexOrThrow("title"));
        }
        public String getColEmployerName{
            return getString(getColumnIndexOrThrow("employer_name"));
        }
        public long getColLatitude{
            return getLong(getColumnIndexOrThrow("latitude"));
        }
        public long getColLongitude{
            return getLong(getColumnIndexOrThrow("longitude"));
        }
        public long getColStatus{
            return getLong(getColumnIndexOrThrow("status"));
        }
    }
  

以下是一些重點代碼解釋:

1 基於用戶請求的排序列(sortBy的參數)的查詢函數,作為游標返回結果。

2 創建查詢字符串。大多數字符串是靜態的(QUERY變量類型),但是該行執行列排序。即使QUERY是private類型,封閉類還是可以用它。因為getJobs方法和JobsCursor類都在MicroJobsDatabase類內,這使得JobsCursor的private類型的數據成員在getJobs方法中也可以訪問。

要獲取sort列的文本,只需要對傳遞給調用函數的枚舉參數運行toString方法。可以定義關聯數組,它可以提供對變量進行命名的更大的靈活性,但是其解決方案也更簡單。此外,使用IDE的自動補全功能,很容易彈出列的名稱。

3 返回數據庫的句柄。

4 使用SQLiteDatabase對象的rawQueryWithFactory方法創建JobsCursor游標。該方法允許傳遞一個factory方法,Android會使用該方法創建需要的準確的游標類型。如果使用了更簡單的rawQuery方法,則將獲取到通用的Cursor,它缺乏JobsCursor的特性。

5 為了便於調用,游標指向結果的第一條記錄。這樣游標就易於使用。一個常見的錯誤是忘記執行moveToFirst調用,導致怎麼也找不出Cursor對像為何拋出異常。

6 返回值是游標。

7 getJobs方法返回創建游標的類。

8 提供可選的排序條件的方式:在枚舉enum中保存列的名稱。在第2項中使用enum類型。

9 定制的游標的構造函數。最後一個參數是調用時所傳遞的查詢。

十 創建游標的Factory類,嵌入在JobsCursor類中。

 根據調用傳遞的查詢創建的游標。

 返回指向封裝的JobsCursor類的游標。

 從游標下面的行抽取特定列的函數。例如,getColTitle返回游標當前引用的記錄的title列的值。它把數據庫實現和調用代碼分離,使得代碼便於閱讀。

注意:雖然在單個應用中,繼承cursor是使用數據庫的一種不錯的方式,但它不適用於內容提供者API,因為Android不支持cursor子類進程間共享。此外,MJAndroid應用是構造的示例,用來說明如何使用數據庫。在第13章,我們給出的應用包含更健壯的架構,你可能會在生產應用中看到這樣的架構。

關於數據庫的使用範例如下所示(該代碼獲得游標,按標題排序,通過getJobs執行調用。然後,它對職位進行迭代遍歷):


MicroJobsDatabase db = new MicroJobsDatabase(this);
1
JobsCursor cursor = db.getJobs(JobsCursor.SortBy.title);
2
for (int rowNum = 0; rowNum < cursor.getCount; rowNum++) {
3
    cursor.moveToPosition(rowNum);
    doSomethingWith(cursor.getColTitle);
4
}
  

以下是一些重點代碼解釋:

1 創建對像MicroJobsDatabase。參數this表示之前所述的上下文。

2 創建游標JobsCursor,指向前面提到的SortBy枚舉對象。

3 使用通用的Cursor方法對游標進行迭代。

4 在循環內,調用其中一個JobsCursor提供的自定義accessor方法,通過每條記錄的title列執行用戶選定的事項。

使用query方法

雖然對於需要執行較複雜的數據庫操作的應用,如前所示,把SQL語句分離開是必要的,但它對於包含簡單的數據庫操作的應用也是很方便的,例如SimpleFinchVideoContentProvider利用SQLiteDatabase.query方法,如下視頻相關的示例所示:


videoCursor = mDb.query(VIDEO_TABLE_NAME, projection,
    where, whereArgs,
    null, null, sortOrder);
  

對於前面所示的SQLiteDatabase.rawQueryWithFactory,query方法的返回值是一個Cursor對象。把該游標賦值給前面定義的videoCursor變量。

query方法在給定表上執行SELECT,在這個例子中常量是VIDEO_TABLE_NAME。query方法接受兩個參數。首先,query中只應該顯示給出名字的列——其他列不應該在游標結果中顯示。對於很多應用,該參數可以接受null值,它會導致結果游標中顯示所有的列值。然後,where參數包含SQL where語句,而不需要WHERE關鍵字。Where參數也可以包含多個'?'字符串,它會被whereArgs值所取代。當我們探討execSQL方法時,將詳細探討這兩個值是如何結合起來的。

修改數據庫

當想要從數據庫中讀取數據時,Android的游標是非常有用的,但是類android.database.Cursor並沒有提供方法來創建、更新或刪除數據。SQLiteDatabase類提供兩個基礎的API,可以用它們執行讀數據和寫數據操作:

·Insert、query、update和delete方法

·更常用的execSQL方法,它接受任何單個SQL語句,它不返回數據,並且在數據庫上運行

如果第一組操作合適,建議使用第一種方式。我們將向你介紹兩種使用MJAndroid操作的方式。

向數據庫中插入數據

當想要向SQL數據庫插入數據時,就使用SQL INSERT語句。INSERT語句相當於CRUD理念的創建表create操作。

在MJAndroid應用中,當用戶查看職位列表時,可以通過單擊Add Job菜單項把職位添加到列表中。然後,可以填寫表單,輸入employer、job title和description信息。當用戶單擊表單的Add Job按鈕後,執行以下代碼:


db.addJob(employer.id, txtTitle.getText.toString,
    txtDescription.getText.toString);
  

該代碼調用addJob函數,傳遞employer ID、job title和job description。addJob函數執行把job寫入數據庫的實際工作。

以下示例說明了insert方法的使用:


/**
 * Add a new job to the database. The job will have a status of open.
 * @param employer_id       The employer offering the job
 * @param title             The job title
 * @param description       The job description
 */
public void addJob(long employer_id, String title, String description) {
    ContentValues map = new ContentValues;
1
    map.put("employer_id", employer_id);
    map.put("title", title);
    map.put("description", description);
    try{
        getWritableDatabase.insert("jobs", null, map);
2
    } catch (SQLException e) {
        Log.e("Error writing new job", e.toString);
    }
}
  

以下是一些重點代碼解釋:

1 ContentValues對象是列名到列值的映射。在代碼內部,是作為HashMap<String,Object>實現的。但是,和簡單的HashMap不同,ContentValues是強類型(strongly types)的。可以指定保存在ContentValues容器中的每個值的數據類型。當從ContentValues容器中讀取這些值時,ContentValues會自動把值轉換成請求的類型。

2 insert方法的第二個參數是nullColumnHack。只有當第三個參數map的值是null時,它才使用默認值,這樣該記錄就完全為空。

使用execSQL方法。該解決方案在更低層次上工作。它創建SQL,把它傳遞給庫來執行。即使可以對每條語句硬編碼,包括用戶傳遞的數據,還是建議最好使用bind參數的方式。

bind參數是一個問號,它保存SQL語句的一個字符,通常是用戶傳遞的參數,如WHERE子句中的值。在通過bind參數創建SQL語句後,可以重複使用它,每次執行前設置bind參數的實際值:


/**
 * Add a new job to the database. The job will have a status of open.
 * @param employer_id       The employer offering the job
 * @param title             The job title
 * @param description       The job description
 */
public void addJob(long employer_id, String title, String description){
    String sql =
1
        "INSERT INTO jobs " +
        "(_id, employer_id, title, description, start_time, end_time, status) " +
        "VALUES " +
        "(NULL, ?,           ?,     ?,           0,          0,       3)";
    Object bindArgs = new Object{employer_id, title, description};
    try{
        getWritableDatabase.execSQL(sql, bindArgs);
2
    } catch (SQLException e) {
        Log.e("Error writing new job", e.toString);
    }
}
  

以下是一些重點代碼的解釋:

1 構建名為sql的SQL查詢模板,它包含可綁定的參數,用於接收用戶數據。可綁定的參數是通過字符串中的問號來表示的。下一步,將構建名為bindArgs的對象數組,在SQL模板中,每個元素包含一個對象。在模板中有3個問號,因此在對像數組中應該有3個元素。

2 通過傳遞SQL模板字符串和綁定到execSQL的參數來執行SQL命令。

使用SQL模板和綁定參數的方式要遠遠好於在String或StringBuilder中填充參數構建SQL語句。通過使用包含參數的模板,可以規避應用中存在的SQL注入攻擊風險。當某公惡意用戶輸入信息到表單中故意惡意修改數據庫時就會發生這些攻擊。攻擊者通常是通過提前結束當前的SQL命令,使用SQL語法字符,然後直接在表單中添加新的SQL命令來進行攻擊。模板-參數方式還可以避免運行時錯誤,例如參數中不正確的字符。這種方式還使得代碼更乾淨,因為它避免了通過自動替換問號,手工追加字符串出現的長串問題。

更新已經在數據庫中的數據

MicroJobs應用支持用戶通過單擊Jobs列表中的job並選擇Edit Job菜單項來編輯job。然後,用戶可以修改editJob表單的employer、job title和description字符串。當用戶單擊表單上的Update按鈕時,會執行以下代碼行:


db.editJob((long)job_id, employer.id, txtTitle.getText.toString,
  txtDescription.getText.toString);
  

這塊代碼調用editJob方法,傳遞job ID和用戶可以改變的3個項:employer ID、job title和job description。editJob方法執行真正的修改數據庫中的job工作。

使用update方法。以下示例說明了update方法的使用:


/**
 * Update a job in the database.
 * @param job_id        The job id of the existing job
 * @param employer_id       The employer offering the job
 * @param title        The job title
 * @param description       The job description
 */
public void editJob(long job_id, long employer_id, String title, String description)
{
    ContentValues map = new ContentValues;
    map.put("employer_id", employer_id);
    map.put("title", title);
    map.put("description", description);
    String whereArgs = new String{Long.toString(job_id)};
    try{
        getWritableDatabase.update("jobs", map, "_id=?", whereArgs);
1
    } catch (SQLException e) {
        Log.e("Error writing new job", e.toString);
    }
}
  

以下是一些重點代碼的解釋:

1 要更新的第一個參數是要操作的表的名稱。第二個參數是把列名映射到新值。第三個參數是一塊SQL代碼。在這個例子中,它是包含一個參數的SQL模板。該參數包含一個問號,其通過第四個參數的內容複製。

使用execSQL方法。以下示例說明了execSQL方法的使用:


/**
 * Update a job in the database.
 * @param job_id             The job id of the existing job
 * @param employer_id       The employer offering the job
 * @param title             The job title
 * @param description       The job description
 */
public void editJob(long job_id, long employer_id, String title, String description)
{
    String sql =
        "UPDATE jobs " +
        "SET employer_id = ?, "+
        " title = ?, "+
        " description = ? "+
        "WHERE _id = ? ";
    Object bindArgs = new Object{employer_id, title, description, job_id};
    try{
        getWritableDatabase.execSQL(sql, bindArgs);
    } catch (SQLException e) {
        Log.e("Error writing new job", e.toString);
    }
}
  

對於這個示例應用,顯示的是最簡單的可能功能。這樣在書中可以便於理解,但是對於真實的應用是不夠的。在真實的應用中,需要檢查輸入字符串中的無效字符,在嘗試更新job之前校驗job存在與否,在使用employer_id之前校驗其值有效與否,更好地捕捉錯誤,等等。還可能對多人共享的應用,進行用戶身份驗證。

刪除數據庫中的數據

MicroJobs應用不但支持用戶刪除job,而且支持用戶創建和修改job。從主應用界面看,用戶可以單擊List Jobs按鈕,獲得職位列表,然後單擊某個特定job來查看該job詳細信息。在這個階段,用戶可以單擊Delete this job菜單按鈕刪除job。應用會詢問確定用戶是否真的想要刪除job。當用戶單擊Delete按鈕時,會執行MicroJobsDetail.java文件中的以下代碼塊:


db.deleteJob(job_id);
  

這行代碼調用MicroJobsDatabase類的deleteJob方法,傳遞要刪除的job ID給該方法。這塊代碼和我們之前看到的函數類似,同樣缺乏現實功能。

使用delete方法。以下代碼說明了delete方法的使用:


/**
 * Delete a job from the database.
 * @param job_id       The job id of the job to delete
 */
public void deleteJob(long job_id) {
    String whereArgs = new String{Long.toString(job_id)};
    try{
        getWritableDatabase.delete("jobs", "_id=?", whereArgs);
    } catch (SQLException e) {
        Log.e("Error deleteing job", e.toString);
    }
}
  

使用execSQL方法。以下示例說明了execSQL方法的使用:


/**
 * Delete a job from the database.
 * @param job_id       The job id of the job to delete
 */
public void deleteJob(long job_id) {
    String sql = String.format(
            "DELETE FROM jobs " +
            "WHERE _id = '%d' ",
            job_id);
    try{
        getWritableDatabase.execSQL(sql);
    } catch (SQLException e) {
        Log.e("Error deleteing job", e.toString);
    }
}