(译)从Content Provider中获取数据

原文:https://developer.android.google.cn/guide/topics/providers/content-provider-basics.html

Content Provider基础

Content Provider管理对中央数据仓库的访问,它是Android应用的一部分,通常会提供自己的UI来使用数据。然而,Content Provider主要是为了给其他应用使用而设计的,这些应用可以通过Provider客户端程序访问内容提供者。Provider和Provider客户端共同提供了一个稳定的、标准的访问数据的接口,并且也很好地解决了进程间通信和安全访问数据的问题。
一般来说,你会在这两种场合下使用Content Provider:想要访问其他程序中一个已有的Content Provider,或是想要在应用中创建一个新的Content Provider来和其他应用分享数据。
本文主要讲述了以下几点:

  • Content Provider是如何工作的。
  • 从Content Provider中获取数据的API。
  • 插入、更新、删除Content Provider中的数据的API。
  • 其他能够让你更加方便地使用Content Provider的API。

概述

Content Provider利用若干张表来向外部应用提供数据,这些表的形式类似于关系型数据库中的表。表中的每行代表Provider收集的某类数据的一个实例,每列代表数据的一个部分。
Content Provider通过众多的API和构建管理到应用的数据存储层的访问,包括:

  • 与其他应用分享你的应用中的数据。
  • 向控件发送数据。
  • 利用搜索框架,通过SearchRecentSuggestionsProvider向你的应用返回定制的搜索建议。
  • 通过实现AbstractThreadedSyncAdapter同步你的服务器和应用的数据。
  • 通过CursorLoader为你的UI加载数据。

(译)从Content Provider中获取数据_第1张图片

访问一个Content Provider

当你想要访问Content Provider中的数据时,你需要使用你的应用的Context中的ContentResolver对象,它可以作为一个客户端来与Provider进行交流。和ContentResolver对象进行交流的是ContentProvider的实现类的一个实例。这个Provider实例从客户端接收数据请求,执行被请求的行为,并最终返回结果。ContentResolver类提供了对永久存储的基础的增删改查的方法。
使用CursorLoader执行异步查询是从UI访问 ContentProvider对象的一个常用模式。UI中的Activity或者Fragment通过CursorLoader执行查询操作,并最终利用ContentResolver获得ContentProvider对象。这让你的UI在查询过程中也能和用户交互。这个模式涉及到一系列的对象,如下图所示:
(译)从Content Provider中获取数据_第2张图片
注意:想要访问Provider,你的应用通常需要在manifest文件中声明相应的权限。
用户词典是Android平台的一个内嵌的Provider,它存储了用户想要保存的一些非常规的单词。
表1:

word app id frequency locale _ID
mapreduce user1 100 en_US 1
precompiler user14 200 fr_FR 2
applet user2 225 fr_CA 3
const user1 255 pt_BR 4
int user5 100 en_UK 5

表1中,每行代表一个单词,这个单词可能不能在标准字典中找到。每列代表这个单词的某些方面的信息,比如首次遇到的地点。每列的头是它们在Content Provider中的列名。这个Provider中,_ID列充当了主键的角色,它是Provider自动保持的。
为了从用户词典中获取单词和它们的地点,你需要调用ContentResolver.query()。这个方法会调用用户词典Provider定义的ContentProvider.query()方法。下面是使用ContentResolver.query()的一个示例:

// 查询用户词典并返回结果
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,   // 单词表的内容URI
    mProjection,                        // 需要返回的列
    mSelectionClause                    // 选取准则
    mSelectionArgs,                     // 选取准则
    mSortOrder);                        // 行间的排序方法

下面介绍 query(Uri,projection,selection,selectionArgs,sortOrder) 方法的参数是如何和SQL SELECT语句匹配的:
(译)从Content Provider中获取数据_第3张图片

内容URI

内容URI是一个标识Provider中的数据的URI,它包括整个Provider的符号名(授权机构)、指向的表的名称(路径)。当你调用客户端方法访问Provider中的一张表时,内容URI是一个必要的参数。
在前面的代码中,CONTENT_URI常量包含了用户词典的内容URI。ContentResolver对象会分析出URI的授权机构,并用它来解析出对应的Provider。之后,ContentResolver就能将查询分发到正确的Provider处。
ContentProvider通过内容URI的路径部分选择需要访问的表。Provider一般会为每张表准备一个路径。
在前面的代码中,用户词典完整的URI为:

content://user_dictionary/words

user_dictionary代表授权机构,words代表路径。content://代表协议,用于标明这是一个内容URI。
许多的Provider允许你通过在URI的最后追加一个ID值来访问某个单独的行。比如,如果你想要访问_ID==4的行,可以这么做:

Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);

注意:Uri与Uri.Builder类包含了一些用于构建形式良好的Uri对象的方法。 ContentUris类包含了为URI追加ID的方法,比如上面的withAppendedId()。

从Provider中获取数据

这部分描述了如何从Provider中获取数据,同样使用用户词典的例子。
注意:为了保持简洁,这部分的代码片段都在UI线程中调用了 ContentResolver.query()方法。在实际的程序中,你应当在一个单独的线程中执行异步查询。一种实现的方法是使用CursorLoader类。并且,这里演示用的代码都只是一些片段,不是一个完整的应用。
想要从Provider中获取数据,需要以下两个步骤:

  1. 获取Provider的读取访问权限;
  2. 构建在Provider中进行查询的代码。

获取读取权限

为了从Provider中获取数据,你的应用需要该Provider的读取访问权限。这个权限不能在运行时获取,只能在Manifest文件中通过元素确定。当用户安装你的应用时,就表示它们同意这个权限要求。
为了找到你需要使用的Provider所需的权限的名称,你需要去查看这个Provider的文档。例如,用户词典Provider需要的权限名为android.permission.READ_USER_DICTIONARY,如果你想要读取用户词典的内容,就必须在Manifest文件中声明该权限。

构建查询代码

获取数据的下一步是构建查询代码。下面的代码片段定义了一些用于访问用户词典的变量:

// 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()。Provider的客户端查询代码和SQL语句非常接近,它包含了需要被返回的列的集合、选取准则和排序方法。
需要被返回的列的集合也称为投影(projection),对应mProjection变量。
SELECT从句和SELECT参数定义了需要被返回的行的特征。SELECT从句是一系列逻辑和布尔表达式、列名称和值(mSelectionClause)的组合。如果你指定了占位符‘?’而不是具体值,查询方法会从SELECT参数数组(mSelectionArgs)中获得值。
下面的片段中,如果用户没有输入一个单词,SELECT从句就会被设为null,并且此次查询会返回用户词典中的所有单词。如果用户输入了一个单词,SELECT从句就会被设为UserDictionary.Words.WORD + ” = ?” ,并且SELECT参数数组的第一个元素会被设为用户输入的单词。

/*
 * 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 =  
ORDER BY word ASC;

防止恶意输入

如果该Provider使用一个SQL数据库来管理数据,那么将不受信任的数据包含在原始SQL语句中会导致SQL注入。
考虑这样的SELECT从句:

// Constructs a selection clause by concatenating the user's input to the column name
String mSelectionClause =  "var = " + mUserInput;

如果这么做,用户将拥有将恶意的SQL语句和你的SQL语句串联起来的能力。比如,用户可以输入“nothing; DROP TABLE ;”作为mUserInput的值,这会导致最终的SQL语句变为“var = nothing; DROP TABLE ;”。由于SELECT从句会被作为SQL语句对待,这可能会导致Provider删除数据库中的所有表(除非Provider设置了防止SQL注入的方法)。
为了避免这个问题,需要在SELECT子句中使用“?”占位符,并提供一个单独的SELECT参数列表。这样做的话,用户的输入会和查询绑定起来,而不会被视为SQL语句的一部分。由于不会被当成SQL语句,自然也就避免了SQL注入问题。上面的例子可以用这样的SELECT子句代替:

// Constructs a selection clause with a replaceable parameter
String mSelectionClause =  "var = ?";

SELECT参数数组:

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

可以这样来向SELECT参数数组中添加值:

// Sets the selection argument to the user's input
selectionArgs[0] = mUserInput;

使用SELECT子句搭配SELECT参数的方法要比直接指定SELECT语句
好,即使Provider使用的不是SQL数据库。

展示查询结果

客户端方法ContentResolver.query()会返回一个Cursor,它包含投影指定的列以及满足选取准则的行。Cursor提供了对各行各列的随机访问。使用Cursor提供的方法,你可以遍历结果的所有行,确定每列的数据类型,从各列中获得数据,并且检查结果的其他属性。一些Cursor实现能够在Provider的数据改变时自动更新,或者在Cursor发生变化时触发某些观察者中的方法。
注意:一些Provider可能会限制对它的某些列的访问,基于进行查询的对象。
如果没有任何行能满足选取准则的要求,那么Provider会返回一个空Cursor对象,调用它的getCount()方法会返回0。
如果出现内部错误,那么查询的结果会根据具体的Provider而定。可能会返回null或者抛出异常。
由于Cursor通常包含许多行,可以利用ListView来展示它的内部数据,通过SimpleCursorAdapter。
接着上面的代码片段,下面的片段中创建了一个包含查询结果的 SimpleCursorAdapter,充当ListView的适配器。

// Defines a list of columns to retrieve from the Cursor and load into an output row
String[] mWordListColumns =
{
    UserDictionary.Words.WORD,   // Contract class constant containing the word column name
    UserDictionary.Words.LOCALE  // Contract class constant containing the locale column name
};

// Defines a list of View IDs that will receive the Cursor columns for each row
int[] mWordListItems = { R.id.dictWord, R.id.locale};

// Creates a new SimpleCursorAdapter
mCursorAdapter = new SimpleCursorAdapter(
    getApplicationContext(),               // The application's Context object
    R.layout.wordlistrow,                  // A layout in XML for one row in the ListView
    mCursor,                               // The result from the query
    mWordListColumns,                      // A string array of column names in the cursor
    mWordListItems,                        // An integer array of view IDs in the row layout
    0);                                    // Flags (usually none are needed)

// Sets the adapter for the ListView
mWordList.setAdapter(mCursorAdapter);

注意:为了利用Cursor向ListView提供诗句,查询中必须包含_ID列,ListView没有把它展示出来。这也是许多Provider提供了_ID列的原因。

从查询结果中获取数据

除了简单地将查询结果展示出来之外,你也可以在其他任务中使用它们。比如,你可以从用户词典中获取单词拼写,并在其他Provider中进行查询。为了这么做,你需要遍历Cursor的所有行:

// Determine the column index of the column named "word"
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);

/*
 * Only executes if the cursor is valid. The User Dictionary Provider returns null if
 * an internal error occurs. Other providers may throw an Exception instead of returning null.
 */

if (mCursor != null) {
    /*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you will get an
     * exception.
     */
    while (mCursor.moveToNext()) {

        // Gets the value from the column.
        newWord = mCursor.getString(index);

        // Insert code here to process the retrieved word.

        ...

        // end of while loop
    }
} else {

    // Insert code here to report an error if the cursor is null or the provider threw an exception.
}

Cursor对象实现了一系列的get方法,用于从该对象中获取不同类型的数据。上面的代码中使用的是getString(),你也可以用getType()来获取某列的数据类型。

Content Provider权限

Provider所属的应用可以指定一系列的权限,其他应用在获取数据之前必须要获取这些权限。这些权限保证用户能够知道某个应用正在获取什么类型的数据。用户会在安装应用时了解到应用需要哪些权限。
如果Provider没有声明任何权限,那么除了它的内部构件之外,任何应用都无法获得它的内部数据。
比如,想要从用户词典中获取数据的话,需要 android.permission.READ_USER_DICTIONARY权限,而想要进行增删改操作的话还需要 android.permission.WRITE_USER_DICTIONARY权限。
权限需要在Manifest文件中声明:

 <uses-permission android:name="android.permission.READ_USER_DICTIONARY">

这些权限会在安装应用时向用户申请。除非用户全部同意,否则安装不会继续进行。

你可能感兴趣的:(Android,系统组件)