星期三, 8月 27, 2014

Content Providers Basics (一)

如何存取Content provider中的資料


Overview

一個content provider提供資料給外部應用程式是一個或多個資料表,類似關聯式資料庫的資料表。一行表示是provider蒐集某些類型資料的一個實例,一行中的每個列表示對於一個實例資料的個別片段。

例如,在Android平台中的內建的provider之一的user dictionay,user dictionary儲存了使用者想保留的非標準單字的拼法,以下表格說明在provider的資料表中資料的樣子。

Table 1: Sample user dictionary table.
wordapp idfrequencylocale_ID
mapreduceuser1100en_US1
precompileruser14200fr_FR2
appletuser2225fr_CA3
constuser1255pt_BR4
intuser5100en_UK5
在資料表中,每一行表示每個單字的實例,單字可能不存在標準字典中。每一列表示對於此單字的一些資料,如locale。列的標頭表示列的名稱,儲存在provider中。參考row的locale,就參考到locale列。對於此資料表來說,_ID列就如"主鍵"列一樣,由provider自動地維護。

注意 - provider不要求要有主鍵,且如果資料表有主鍵也不一定要用_ID為名稱。然而,如果你想要從provider將資料綁定在ListView上,其中一個列名稱一定要是_ID。詳細原因請繼續往下到Displaying query results。


Accessing a provider

應用程式用一個ContentResolver客戶端物件來存取provider中的資料。ContentResovler物件有方法,它們會呼叫在provider物件中同名的方法。ContentResolver方法提供基本的"CRUD"(create、read、update及delete)功能。

在客戶端應用程式的處理程序中的ContentResolver物件和在擁有provider的應用程式中的ContentProvider物件會自動地處置處理程序間的通信,ContentProvider也會扮演一個在資料的資料庫與資料的外部顯示如資料表之間的抽象層。

注意 -存取provider時,你的應用程式必須在manifest文件中請求特定的權限,詳細說明在繼續往下到Content Providers Permission。

舉例,從User Dictionary Provider中得到單字及它們的locale的列表,你會呼叫ContentResolver.query()。此query()方法呼叫由User Dictionary Provider定義的query()方法。以下顯示ContentResolver.query()的呼叫。

// Queries the user dictionary and returns results
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,   // The content URI of the words table
    mProjection,                        // The columns to return for each row
    mSelectionClause                    // Selection criteria
    mSelectionArgs,                     // Selection criteria
    mSortOrder);                        // The sort order for the returned rows
表格2顯示query()中的參數對應到SQL SELECT描述。

Table 2: Query() compared to SQL query.
query() argumentSELECT keyword/parameterNotes
UriFROM table_nameUri maps to the table in the provider named table_name.
projectioncol,col,col,...projection is an array of columns that should be included for each row retrieved.
selectionWHERE col =valueselection specifies the criteria for selecting rows.
selectionArgs(No exact equivalent. Selection arguments replace? placeholders in the selection clause.)
sortOrderORDER BYcol,col,...sortOrder specifies the order in which rows appear in the returnedCursor.


Content URIs

content URI是辨識provider中資料的一種URI,Content URIs包含整個provider的符號名稱(它的authority)且一個名稱點出一個資料表(一個path)。當你呼叫一個客戶端方法存取一個provider的一個資料表時,對於資料表來說,content URI是其中一個參數。

在先前的程式碼中,常數CONTENT_URI含有user dictionary的"word"資料表的content URI,ContentResolver物件解析出URI的authority,並利用它藉由與已知的provider的系統資料表的authority比較去"Resolve"provider,接著ContentResolver發送查詢參數給正確的provider。

ContentResolver利用content URI的一部分路徑來選擇存取的資料表。一個provider通常為每個資料表有一個path

在先前的程式碼中,"word"資料表的URI為

content://user_dictionary/words
user_dictionary字串是provider的authority,且words字串是資料表的路徑,字串content://(the scheme)總是存在的,且用來識別這是一個content URI。

許多provider允許你藉由在URI的末端附加上ID值來存取資料表中的單一行。例如,擷取自user dictionary它的_ID為4的一整行,可以使用以下的content URI

Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
當你已經擷取出一組行並想要更新或刪除它們其中的一行時,你可以使用id值達成。

注意 -Uri和Uri.Binder類別包含從字串建立良好格式的Uri的便利方法,ContentUris包含在一個Uri上附加id值得便利方法,閒錢的片段是使用withAppendedId()方法附加id值到USER DICTIONARY的content URI。


Retrieving Data from the Provider

接下來是要了解如何從一個provider中擷取資料。

擷取來自provider的資料,遵循這些基本步驟

  1. 對provider請求讀取權限
  2. 定義寄給provider的查詢的程式碼

Requesting read access permission

擷取來自provider的資料,對於provider你的應用程式需要"read access permission",無法在執行時請求此權限,你必須使用<uses-permission>標籤及確切由provider定義的名稱,在manifest文件指定你需要此權限。當你在manifest文件中指定此標籤,你實際上為你的應用程式"請求"

此權限。當使用者安裝你的應用程式,他們默默地授予此請求。

找出對於你使用的provider的read access permission的確切名稱,以及provider的其他存取權限名稱,請查看p該rovider的文件。

詳細在存取provider的權限的角色細節,請往下到Content Provider Permissions。

USER DICTIONARY PROVIDER定義了android.permission.READ_USER_DICTIONARY權限在manifeest文件中,所以應用成是想要讀取來自provider的資料必須請求此權限。


Constructing the query

擷取來自provider的資料的下一步是建購查詢(query),第一個片段是為存取User Dictionary Provider而定義一些變數。


// A "projection" defines the columns that will be returned for each row
String[] mProjection =
{
    UserDictionary.Words._ID,    // Contract class constant for the _ID column name
    UserDictionary.Words.WORD,   // Contract class constant for the word column name
    UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
};
// Defines a string to contain the selection clause
String mSelectionClause = null;
// Initializes an array to contain selection arguments
String[] mSelectionArgs = {""};
下一個片段顯示如何利用ContentResolver.query(),以User Dictionary Provider為例。provider客戶端查詢與SQL查詢相似,它包含一組回傳欄位集合,一組selection條件及排序。

查詢回傳的欄位集合是被稱為projection(如變數mProjection)。

擷取指定的行的表示式被分為selection clause和selection argument,selection clause是結合了邏輯和布林的表示式、欄位名稱及值(如變數mSelectionClause),如果你只訂了可更換參數?取代值,query方法就會從selection argument陣列中取出值來取代它(如變數mSelectionArgs)。

下一個片段,如果使用者不輸入單字,selection clause設定為null,查詢會回傳所有在provider中的單字。如果使用者輸入單字,selection clause設定為UserDictionary.Words.WORD + " = ?"且selection argument陣列的第一個元素設定為使用者輸入的單字。

/*
 * This defines a one-element String array to contain the selection argument.
 */
String[] mSelectionArgs = {""};
// Gets a word from the UI
mSearchString = mSearchWord.getText().toString();
// Remember to insert code here to check for invalid or malicious input.
// If the word is the empty string, gets everything
if (TextUtils.isEmpty(mSearchString)) {
    // Setting the selection clause to null will return all words
    mSelectionClause = null;
    mSelectionArgs[0] = "";
} else {
    // Constructs a selection clause that matches the word that the user entered.
    mSelectionClause = UserDictionary.Words.WORD + " = ?";

    // Moves the user's input string to the selection arguments.
    mSelectionArgs[0] = mSearchString;
}
// Does a query against the table and returns a Cursor object
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
    mProjection,                       // The columns to return for each row
    mSelectionClause                   // Either null, or the word the user entered
    mSelectionArgs,                    // Either empty, or the string the user entered
    mSortOrder);                       // The sort order for the returned rows
// Some providers return null if an error occurs, others throw an exception
if (null == mCursor) {
    /*
     * Insert code here to handle the error. Be sure not to use the cursor! You may want to
     * call android.util.Log.e() to log this error.
     *
     */
// If the Cursor is empty, the provider found no matches
} else if (mCursor.getCount() < 1) {

    /*
     * Insert code here to notify the user that the search was unsuccessful. This isn't necessarily
     * an error. You may want to offer the user the option to insert a new row, or re-type the
     * search term.
     */
} else {
    // Insert code here to do something with the results
}
此查詢類似SQL描述。

SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

Protecting against malicious input

如果由content provider管理的資料是在SQL資料庫中,包含外部不可信的資料進入SQL描述可以導致SQL injection。

考慮此selection clause

// Constructs a selection clause by concatenating the user's input to the column name
String mSelectionClause =  "var = " + mUserInput;
如果你這麼做,你正允許使用者串連惡意SQL到你的SQL描述上。例如,使用者可以對mUserInput輸入"nothing; DROP TABLE *;",它會導致selection clause為var = nothing; DROP TABLE *;。因為selection clause被視為SQL描述,這可能導致provider刪除在底層SQLite資料庫中的所有資料表(除非provider被設定嘗試捕捉SQL injection)。

為了避免此問題,使用有利用?作為可更換變數的selection clause和selection arguments的獨立陣列。當你這麼做時,使用者輸入是直接綁在查詢上,而不是被直譯為SQL描述的一部份。因為它不被視為SQL,使用者輸入無法注入惡意SQL,取代使用串連方式來包含使用者輸入,使用此selection clause。

// Constructs a selection clause with a replaceable parameter
String mSelectionClause =  "var = ?";
設定selection arguments陣列如下

// Defines an array to contain the selection arguments
String[] selectionArgs = {""};



沒有留言:

張貼留言